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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FwDataMiniLcmBridge.Tests.Fixtures;

namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;

[Collection(ProjectLoaderFixture.Name)]
public class QueryEntryTests(ProjectLoaderFixture fixture) : QueryEntryTestsBase
{
protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("query-entry-test", "en", "en"));
}
}
52 changes: 18 additions & 34 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System.Text;
using FwDataMiniLcmBridge.Api.UpdateProxy;
using FwDataMiniLcmBridge.LcmUtils;
using Gridify;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Exceptions;
using MiniLcm.Models;
Expand All @@ -18,8 +20,15 @@

namespace FwDataMiniLcmBridge.Api;

public class FwDataMiniLcmApi(Lazy<LcmCache> cacheLazy, bool onCloseSave, ILogger<FwDataMiniLcmApi> logger, FwDataProject project, MiniLcmValidators validators) : IMiniLcmApi, IMiniLcmSaveApi
public class FwDataMiniLcmApi(
Lazy<LcmCache> cacheLazy,
bool onCloseSave,
ILogger<FwDataMiniLcmApi> logger,
FwDataProject project,
MiniLcmValidators validators,
IOptions<FwDataBridgeConfig> config) : IMiniLcmApi, IMiniLcmSaveApi
{
private FwDataBridgeConfig Config => config.Value;
internal LcmCache Cache => cacheLazy.Value;
public FwDataProject Project { get; } = project;
public Guid ProjectId => Cache.LangProject.Guid;
Expand Down Expand Up @@ -62,43 +71,12 @@

internal WritingSystemId GetWritingSystemId(int ws)
{
return Cache.ServiceLocator.WritingSystemManager.Get(ws).Id;
return Cache.GetWritingSystemId(ws);
}

internal int GetWritingSystemHandle(WritingSystemId ws, WritingSystemType? type = null)
{
var lcmWs = GetLcmWritingSystem(ws, type) ?? throw new NullReferenceException($"Unable to find writing system with id {ws}");
return lcmWs.Handle;
}


internal CoreWritingSystemDefinition? GetLcmWritingSystem(WritingSystemId ws, WritingSystemType? type = null)
{
if (ws == "default")
{
return type switch
{
WritingSystemType.Analysis => WritingSystemContainer.DefaultAnalysisWritingSystem,
WritingSystemType.Vernacular => WritingSystemContainer.DefaultVernacularWritingSystem,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}

var lcmWs = Cache.ServiceLocator.WritingSystemManager.Get(ws.Code);
if (lcmWs is not null && type is not null)
{
var validWs = type switch
{
WritingSystemType.Analysis => WritingSystemContainer.AnalysisWritingSystems,
WritingSystemType.Vernacular => WritingSystemContainer.VernacularWritingSystems,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
if (!validWs.Contains(lcmWs))
{
throw new InvalidOperationException($"Writing system {ws} is not of the requested type: {type}.");
}
}
return lcmWs;
return Cache.GetWritingSystemHandle(ws, type);
}

public Task<WritingSystems> GetWritingSystems()
Expand Down Expand Up @@ -212,7 +190,7 @@
}
await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",
"Revert WritingSystem",
async () =>

Check warning on line 193 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 193 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 193 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 193 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 193 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 193 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var updateProxy = new UpdateWritingSystemProxy(lcmWritingSystem)
{
Expand Down Expand Up @@ -709,6 +687,12 @@

options ??= QueryOptions.Default;
if (predicate is not null) entries = entries.Where(predicate);
if (!string.IsNullOrEmpty(options.Filter?.GridifyFilter))
{
var query = new GridifyQuery(){Filter = options.Filter.GridifyFilter};
var filter = query.GetFilteringExpression(config.Value.Mapper).Compile();
entries = entries.Where(filter);
}

if (options.Exemplar is not null)
{
Expand Down
48 changes: 48 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Globalization;
using MiniLcm.Models;
using SIL.LCModel;
using SIL.LCModel.Core.KernelInterfaces;

namespace FwDataMiniLcmBridge.Api;
Expand Down Expand Up @@ -69,4 +71,50 @@ internal static void ContributeExemplars(ITsMultiString multiString, IReadOnlyDi
}
}
}

internal static WritingSystemId GetWritingSystemId(this LcmCache cache, int ws)
{
return cache.ServiceLocator.WritingSystemManager.Get(ws).Id;
}

internal static int GetWritingSystemHandle(this LcmCache cache, WritingSystemId ws, WritingSystemType? type = null)
{
var wsContainer = cache.ServiceLocator.WritingSystems;
if (ws == "default")
{
return type switch
{
WritingSystemType.Analysis => wsContainer.DefaultAnalysisWritingSystem.Handle,
WritingSystemType.Vernacular => wsContainer.DefaultVernacularWritingSystem.Handle,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}

var lcmWs = cache.ServiceLocator.WritingSystemManager.Get(ws.Code);
if (lcmWs is not null && type is not null)
{
var validWs = type switch
{
WritingSystemType.Analysis => wsContainer.AnalysisWritingSystems,
WritingSystemType.Vernacular => wsContainer.VernacularWritingSystems,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
if (!validWs.Contains(lcmWs))
{
throw new InvalidOperationException($"Writing system {ws} is not of the requested type: {type}.");
}
}
if (lcmWs is null)
{
throw new NullReferenceException($"unable to find writing system with id {ws}");
}

return lcmWs.Handle;
}

internal static string? PickText(this ICmObject obj, ITsMultiString multiString, string ws)
{
var wsHandle = obj.Cache.GetWritingSystemHandle(ws);
return multiString.get_String(wsHandle)?.Text ?? null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ public static void SetMsaPartOfSpeech(this IMoMorphSynAnalysis msa, IPartOfSpeec
return null;
}
}
public static Guid? GetPartOfSpeechId(this IMoMorphSynAnalysis msa)
{
return msa.GetPartOfSpeech()?.Guid;
}
}
7 changes: 7 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/FwDataBridgeConfig.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using FwDataMiniLcmBridge.Api;
using Gridify;
using Microsoft.Win32;
using MiniLcm.Filtering;
using MiniLcm.Models;
using SIL.LCModel;

namespace FwDataMiniLcmBridge;

Expand Down Expand Up @@ -36,4 +41,6 @@ public class FwDataBridgeConfig

public string ProjectsFolder { get; set; } = DataFolder;
public string TemplatesFolder { get; set; } = Path.Join(ProgramFolder, "Templates");

public GridifyMapper<ILexEntry> Mapper { get; set; } = EntryFilter.NewMapper(new LexEntryFilterMapProvider());
}
9 changes: 6 additions & 3 deletions backend/FwLite/FwDataMiniLcmBridge/FwDataFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MiniLcm.Validators;
using SIL.LCModel;

Expand All @@ -14,15 +15,17 @@ public class FwDataFactory(
IMemoryCache cache,
ILogger<FwDataFactory> logger,
IProjectLoader projectLoader,
MiniLcmValidators validators) : IDisposable, IHostedService
MiniLcmValidators validators,
IOptions<FwDataBridgeConfig> config) : IDisposable, IHostedService
{
private bool _shuttingDown = false;
public FwDataFactory(ILogger<FwDataMiniLcmApi> fwdataLogger,
IMemoryCache cache,
ILogger<FwDataFactory> logger,
IProjectLoader projectLoader,
IHostApplicationLifetime lifetime,
MiniLcmValidators validators) : this(fwdataLogger, cache, logger, projectLoader, validators)
MiniLcmValidators validators,
IOptions<FwDataBridgeConfig> config) : this(fwdataLogger, cache, logger, projectLoader, validators, config)
{
lifetime.ApplicationStopping.Register(() =>
{
Expand All @@ -36,7 +39,7 @@ public FwDataFactory(ILogger<FwDataMiniLcmApi> fwdataLogger,

public FwDataMiniLcmApi GetFwDataMiniLcmApi(FwDataProject project, bool saveOnDispose)
{
return new FwDataMiniLcmApi(new (() => GetProjectServiceCached(project)), saveOnDispose, fwdataLogger, project, validators);
return new FwDataMiniLcmApi(new (() => GetProjectServiceCached(project)), saveOnDispose, fwdataLogger, project, validators, config);
}

private HashSet<string> _projects = [];
Expand Down
29 changes: 29 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/LexEntryFilterMapProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Linq.Expressions;
using FwDataMiniLcmBridge.Api;
using MiniLcm.Filtering;
using SIL.LCModel;

namespace FwDataMiniLcmBridge;

public class LexEntryFilterMapProvider : EntryFilterMapProvider<ILexEntry>
{
//used to allow comparing null to an empty list, eg Senses=null should be true when there are no senses
private static object? EmptyToNull<T>(IList<T> list) => list.Count == 0 ? null : list;
private static object? EmptyToNull<T>(IEnumerable<T> list) => !list.Any() ? null : list;
public override Expression<Func<ILexEntry, object?>> EntrySensesSemanticDomains => e => e.AllSenses.Select(s => EmptyToNull(s.SemanticDomainsRC));
public override Expression<Func<ILexEntry, object?>> EntrySensesExampleSentences => e => EmptyToNull(e.AllSenses.SelectMany(s => s.ExamplesOS));
public override Expression<Func<ILexEntry, string, object?>> EntrySensesExampleSentencesSentence => (entry, ws) =>
entry.AllSenses.SelectMany(s => s.ExamplesOS).Select(example => example.PickText(example.Example, ws));

public override Expression<Func<ILexEntry, object?>> EntrySensesPartOfSpeechId =>
e => e.AllSenses.Select(s =>
s.MorphoSyntaxAnalysisRA == null ? null : s.MorphoSyntaxAnalysisRA.GetPartOfSpeechId());
public override Expression<Func<ILexEntry, object?>> EntrySenses => e => EmptyToNull(e.AllSenses);
public override Expression<Func<ILexEntry, string, object?>> EntrySensesGloss => (entry, ws) => entry.AllSenses.Select(s => s.PickText(s.Gloss, ws));
public override Expression<Func<ILexEntry, string, object?>> EntrySensesDefinition => (entry, ws) => entry.AllSenses.Select(s => s.PickText(s.Definition, ws));
public override Expression<Func<ILexEntry, string, object?>> EntryNote => (entry, ws) => entry.PickText(entry.Comment, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLexemeForm => (entry, ws) => entry.PickText(entry.LexemeFormOA.Form, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryCitationForm => (entry, ws) => entry.PickText(entry.CitationForm, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLiteralMeaning => (entry, ws) => entry.PickText(entry.LiteralMeaning, ws);
public override Expression<Func<ILexEntry, object?>> EntryComplexFormTypes => e => EmptyToNull(e.ComplexFormEntryRefs.SelectMany(r => r.ComplexEntryTypesRS));
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.JSInterop;
using MiniLcm;
using MiniLcm.Attributes;
using MiniLcm.Filtering;
using MiniLcm.Models;
using Reinforced.Typings;
using Reinforced.Typings.Ast.Dependency;
Expand Down Expand Up @@ -78,7 +79,7 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
.WithPublicProperties()
.WithPublicMethods(b => b.AlwaysReturnPromise().OnlyJsInvokable());
builder.ExportAsEnum<SortField>().UseString();
builder.ExportAsInterfaces([typeof(QueryOptions), typeof(SortOptions), typeof(ExemplarOptions)],
builder.ExportAsInterfaces([typeof(QueryOptions), typeof(SortOptions), typeof(ExemplarOptions), typeof(EntryFilter)],
exportBuilder => exportBuilder.WithPublicNonStaticProperties());
}

Expand Down
19 changes: 19 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/MiniLcmTests/QueryEntryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace LcmCrdt.Tests.MiniLcmTests;

public class QueryEntryTests : QueryEntryTestsBase
{
private readonly MiniLcmApiFixture _fixture = new();

protected override async Task<IMiniLcmApi> NewApi()
{
await _fixture.InitializeAsync();
var api = _fixture.Api;
return api;
}

public override async Task DisposeAsync()
{
await base.DisposeAsync();
await _fixture.DisposeAsync();
}
}
17 changes: 15 additions & 2 deletions backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq.Expressions;
using FluentValidation;
using Gridify;
using SIL.Harmony;
using SIL.Harmony.Changes;
using LcmCrdt.Changes;
Expand All @@ -9,19 +10,27 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;
using MiniLcm.Exceptions;
using MiniLcm.SyncHelpers;
using MiniLcm.Validators;
using SIL.Harmony.Core;
using SIL.Harmony.Db;
using MiniLcm.Culture;
using MiniLcm.Filtering;

namespace LcmCrdt;

public class CrdtMiniLcmApi(DataModel dataModel, CurrentProjectService projectService, IMiniLcmCultureProvider cultureProvider, MiniLcmValidators validators) : IMiniLcmApi
public class CrdtMiniLcmApi(
DataModel dataModel,
CurrentProjectService projectService,
IMiniLcmCultureProvider cultureProvider,
MiniLcmValidators validators,
IOptions<LcmCrdtConfig> config) : IMiniLcmApi
{
private Guid ClientId { get; } = projectService.ProjectData.ClientId;
public ProjectData ProjectData => projectService.ProjectData;
private LcmCrdtConfig LcmConfig => config.Value;

private IQueryable<Entry> Entries => dataModel.QueryLatest<Entry>();
private IQueryable<ComplexFormComponent> ComplexFormComponents => dataModel.QueryLatest<ComplexFormComponent>();
Expand Down Expand Up @@ -355,7 +364,6 @@ private async IAsyncEnumerable<Entry> GetEntries(
QueryOptions? options = null)
{
options ??= QueryOptions.Default;
//todo filter on exemplar options and limit results, and sort
var queryable = Entries;
if (predicate is not null) queryable = queryable.Where(predicate);
if (options.Exemplar is not null)
Expand All @@ -366,6 +374,11 @@ private async IAsyncEnumerable<Entry> GetEntries(
queryable = queryable.WhereExemplar(ws.Value, options.Exemplar.Value);
}

if (options.Filter?.GridifyFilter != null)
{
queryable = queryable.ApplyFiltering(options.Filter.GridifyFilter, LcmConfig.Mapper);
}

var sortWs = (await GetWritingSystem(options.Order.WritingSystem, WritingSystemType.Vernacular));
if (sortWs is null)
throw new NullReferenceException($"sort writing system {options.Order.WritingSystem} not found");
Expand Down
26 changes: 26 additions & 0 deletions backend/FwLite/LcmCrdt/EntryFilterMapProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Linq.Expressions;
using MiniLcm.Filtering;

namespace LcmCrdt;

public class EntryFilterMapProvider : EntryFilterMapProvider<Entry>
{
public override Expression<Func<Entry, object?>> EntrySensesSemanticDomains => e => e.Senses.Select(s => s.SemanticDomains);
public override Func<string, object>? EntrySensesSemanticDomainsConverter => EntryFilter.ConvertNullToEmptyList<SemanticDomain>;
public override Expression<Func<Entry, object?>> EntrySensesExampleSentences => e => e.Senses.SelectMany(s => s.ExampleSentences);
public override Expression<Func<Entry, string, object?>> EntrySensesExampleSentencesSentence =>
(e, ws)=> e.Senses.SelectMany(s => s.ExampleSentences).Select(example => Json.Value(example.Sentence, ms => ms[ws]));
public override Expression<Func<Entry, object?>> EntrySensesPartOfSpeechId => e => e.Senses.Select(s => s.PartOfSpeechId);
public override Expression<Func<Entry, object?>> EntrySenses => e => e.Senses;

public override Expression<Func<Entry, string, object?>> EntrySensesGloss =>
(entry, ws) => entry.Senses.Select(s => Json.Value(s.Gloss, ms => ms[ws]));
public override Expression<Func<Entry, string, object?>> EntrySensesDefinition =>
(entry, ws) => entry.Senses.Select(s => Json.Value(s.Definition, ms => ms[ws]));
public override Expression<Func<Entry, string, object?>> EntryNote => (entry, ws) => Json.Value(entry.Note, ms => ms[ws]);
public override Expression<Func<Entry, string, object?>> EntryLexemeForm => (entry, ws) => Json.Value(entry.LexemeForm, ms => ms[ws]);
public override Expression<Func<Entry, string, object?>> EntryCitationForm => (entry, ws) => Json.Value(entry.CitationForm, ms => ms[ws]);
public override Expression<Func<Entry, string, object?>> EntryLiteralMeaning => (entry, ws) => Json.Value(entry.LiteralMeaning, ms => ms[ws]);
public override Expression<Func<Entry, object?>> EntryComplexFormTypes => e => e.ComplexFormTypes;
public override Func<string, object>? EntryComplexFormTypesConverter => EntryFilter.ConvertNullToEmptyList<ComplexFormType>;
}
1 change: 1 addition & 0 deletions backend/FwLite/LcmCrdt/LcmCrdt.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Gridify.EntityFramework" Version="2.15.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="linq2db.AspNet" Version="5.4.1"/>
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0" />
Expand Down
6 changes: 5 additions & 1 deletion backend/FwLite/LcmCrdt/LcmCrdtConfig.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
namespace LcmCrdt;
using Gridify;
using MiniLcm.Filtering;

namespace LcmCrdt;

public class LcmCrdtConfig
{
public string ProjectPath { get; set; } = Path.GetFullPath(".");
public GridifyMapper<Entry> Mapper { get; set; } = EntryFilter.NewMapper(new EntryFilterMapProvider());
}
Loading
Loading