diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index d724e90247..9755770cd2 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "9.0.6",
+ "version": "10.0.7",
"commands": [
"dotnet-ef"
],
diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml
index 1d6c2bbdef..ee0fe58067 100644
--- a/.github/workflows/fw-lite.yaml
+++ b/.github/workflows/fw-lite.yaml
@@ -27,7 +27,10 @@ env:
jobs:
build-and-test:
name: Build FW Lite and run tests
- timeout-minutes: 40
+ #bumped from 40 -> 60: with the .NET 10 / Linq2Db v6 upgrade many tests that
+ #used to fast-fail now run to completion (passing), so total test step time
+ #went up. Hits Windows-CI variance; 60 leaves some headroom.
+ timeout-minutes: 60
runs-on: windows-latest
outputs:
version: ${{ steps.setVersion.outputs.VERSION }}
diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props
index bf493b9a70..c54d637395 100644
--- a/backend/Directory.Packages.props
+++ b/backend/Directory.Packages.props
@@ -3,139 +3,142 @@
true
true
$(NoWarn);NU1507
- 9.0.50
- 15.1.10
+ 10.0.60
+ 15.1.16
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
+
diff --git a/backend/FwHeadless/FwHeadless.csproj b/backend/FwHeadless/FwHeadless.csproj
index fe6acf46ad..cda3f10c9e 100644
--- a/backend/FwHeadless/FwHeadless.csproj
+++ b/backend/FwHeadless/FwHeadless.csproj
@@ -11,7 +11,6 @@
-
diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs
index c17c1878ff..79e1cb3ae1 100644
--- a/backend/FwHeadless/Program.cs
+++ b/backend/FwHeadless/Program.cs
@@ -45,7 +45,6 @@
{
//never emit traces for sqlite as there's way too much noise and it'll crash servers and overrun honeycomb
c.Filter = (provider, command) => provider is not "Microsoft.EntityFrameworkCore.Sqlite";
- c.SetDbStatementForText = true;
})
.AddSource(FwHeadlessActivitySource.ActivitySourceName,
FwLiteProjectSyncActivitySource.ActivitySourceName,
diff --git a/backend/FwHeadless/Routes/MediaFileRoutes.cs b/backend/FwHeadless/Routes/MediaFileRoutes.cs
index 5029e40a68..05928ddb57 100644
--- a/backend/FwHeadless/Routes/MediaFileRoutes.cs
+++ b/backend/FwHeadless/Routes/MediaFileRoutes.cs
@@ -7,7 +7,7 @@ public static class MediaFileRoutes
public const string RootRoute = "/api/media";
public static IEndpointConventionBuilder MapMediaFileRoutes(this WebApplication app)
{
- var group = app.MapGroup(RootRoute).WithOpenApi();
+ var group = app.MapGroup(RootRoute);
group.MapGet("/list/{projectId:guid}", MediaFileController.ListFiles);
group.MapGet("/metadata/{fileId:guid}", MediaFileMetadataController.GetFileMetadata);
group.MapGet("/{fileId:guid}", MediaFileController.GetFile);
diff --git a/backend/FwHeadless/Routes/MergeRoutes.cs b/backend/FwHeadless/Routes/MergeRoutes.cs
index d1bd08e84a..126c5451e4 100644
--- a/backend/FwHeadless/Routes/MergeRoutes.cs
+++ b/backend/FwHeadless/Routes/MergeRoutes.cs
@@ -18,7 +18,7 @@ public static class MergeRoutes
{
public static IEndpointConventionBuilder MapMergeRoutes(this WebApplication app)
{
- var group = app.MapGroup("/api/merge").WithOpenApi();
+ var group = app.MapGroup("/api/merge");
group.MapPost("/execute", ExecuteMergeRequest);
group.MapPost("/sync-harmony", SyncHarmonyProject);
diff --git a/backend/FwLite/FwLiteShared/Auth/AuthService.cs b/backend/FwLite/FwLiteShared/Auth/AuthService.cs
index efff36f68f..3a25af68f3 100644
--- a/backend/FwLite/FwLiteShared/Auth/AuthService.cs
+++ b/backend/FwLite/FwLiteShared/Auth/AuthService.cs
@@ -10,7 +10,7 @@ public class AuthService(LexboxProjectService lexboxProjectService, OAuthClientF
[JSInvokable]
public async Task Servers()
{
- return await lexboxProjectService.Servers().ToAsyncEnumerable().SelectAwait(async s =>
+ return await lexboxProjectService.Servers().ToAsyncEnumerable().Select(async (LexboxServer s, CancellationToken _) =>
{
var currentName = await clientFactory.GetClient(s).GetCurrentName();
return new ServerStatus(s.DisplayName,
diff --git a/backend/FwLite/FwLiteWeb/Routes/ActivityRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/ActivityRoutes.cs
index f94563f33e..410b57269d 100644
--- a/backend/FwLite/FwLiteWeb/Routes/ActivityRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/ActivityRoutes.cs
@@ -1,6 +1,6 @@
using LcmCrdt;
using FwLiteWeb.Hubs;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
namespace FwLiteWeb.Routes;
@@ -8,15 +8,15 @@ public static class ActivityRoutes
{
public static IEndpointConventionBuilder MapActivities(this WebApplication app)
{
- var group = app.MapGroup("/api/activity/{project}").WithOpenApi(operation =>
+ var group = app.MapGroup("/api/activity/{project}").AddOpenApiOperationTransformer((operation, _, _) =>
{
- operation.Parameters.Add(new OpenApiParameter()
+ operation.Parameters?.Add(new OpenApiParameter()
{
Name = CrdtMiniLcmApiHub.ProjectRouteKey,
In = ParameterLocation.Path,
Required = true
});
- return operation;
+ return Task.CompletedTask;
});
group.MapGet("/", (HistoryService historyService, int skip, int take) => historyService.ProjectActivity(skip, take));
return group;
diff --git a/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs
index 55bc0711c4..a9617c55d4 100644
--- a/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs
@@ -12,7 +12,7 @@ public static class AuthRoutes
public record ServerStatus(string DisplayName, bool LoggedIn, string? LoggedInAs, string? Authority);
public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app)
{
- var group = app.MapGroup("/api/auth").WithOpenApi();
+ var group = app.MapGroup("/api/auth");
group.MapGet("/servers", (AuthService authService) => authService.Servers());
group.MapGet("/login/{authority}",
async (AuthService authService, string authority, IOptions options, [FromHeader] string referer) =>
diff --git a/backend/FwLite/FwLiteWeb/Routes/FwIntegrationRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/FwIntegrationRoutes.cs
index d1cf29049a..33d4f830b3 100644
--- a/backend/FwLite/FwLiteWeb/Routes/FwIntegrationRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/FwIntegrationRoutes.cs
@@ -3,7 +3,7 @@
using FwLiteWeb.Hubs;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
namespace FwLiteWeb.Routes;
@@ -11,14 +11,14 @@ public static class FwIntegrationRoutes
{
public static IEndpointConventionBuilder MapFwIntegrationRoutes(this WebApplication app)
{
- var group = app.MapGroup($"/api/fw/{{{FwDataMiniLcmHub.ProjectRouteKey}}}").WithOpenApi(
- operation =>
+ var group = app.MapGroup($"/api/fw/{{{FwDataMiniLcmHub.ProjectRouteKey}}}").AddOpenApiOperationTransformer(
+ (operation, _, _) =>
{
- operation.Parameters.Add(new OpenApiParameter()
+ operation.Parameters?.Add(new OpenApiParameter()
{
Name = FwDataMiniLcmHub.ProjectRouteKey, In = ParameterLocation.Path, Required = true
});
- return operation;
+ return Task.CompletedTask;
});
group.MapGet("/link/entry/{id}",
async ([FromServices] FwDataProjectContext context,
diff --git a/backend/FwLite/FwLiteWeb/Routes/HistoryRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/HistoryRoutes.cs
index a1ac74af5d..77660e2452 100644
--- a/backend/FwLite/FwLiteWeb/Routes/HistoryRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/HistoryRoutes.cs
@@ -7,7 +7,7 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using FwLiteWeb.Hubs;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using MiniLcm.Models;
namespace FwLiteWeb.Routes;
@@ -16,13 +16,13 @@ public static class HistoryRoutes
{
public static IEndpointConventionBuilder MapHistoryRoutes(this WebApplication app)
{
- var group = app.MapGroup("/api/history/{project}").WithOpenApi(operation =>
+ var group = app.MapGroup("/api/history/{project}").AddOpenApiOperationTransformer((operation, _, _) =>
{
- operation.Parameters.Add(new OpenApiParameter()
+ operation.Parameters?.Add(new OpenApiParameter()
{
Name = CrdtMiniLcmApiHub.ProjectRouteKey, In = ParameterLocation.Path, Required = true
});
- return operation;
+ return Task.CompletedTask;
});
group.MapGet("/snapshot/{snapshotId:guid}",
async (Guid snapshotId, HistoryService historyService) => await historyService.GetSnapshot(snapshotId));
diff --git a/backend/FwLite/FwLiteWeb/Routes/ImportRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/ImportRoutes.cs
index 10ad36a0ea..eb1939c69a 100644
--- a/backend/FwLite/FwLiteWeb/Routes/ImportRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/ImportRoutes.cs
@@ -1,7 +1,7 @@
using FwLiteShared.Projects;
using SIL.Harmony.Db;
using FwLiteWeb.Services;
- using Microsoft.OpenApi.Models;
+ using Microsoft.OpenApi;
using MiniLcm;
namespace FwLiteWeb.Routes;
diff --git a/backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs
index 8e84c19d4f..988fde5049 100644
--- a/backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs
@@ -1,13 +1,13 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.OpenApi.Any;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using MiniLcm;
using MiniLcm.Filtering;
using MiniLcm.Models;
using MiniLcm.Project;
using MiniLcm.Validators;
+using System.Text.Json.Nodes;
namespace FwLiteWeb.Routes;
@@ -31,9 +31,9 @@ internal IMiniLcmApi MiniLcmApi
public static IEndpointConventionBuilder MapMiniLcmRoutes(this IEndpointRouteBuilder app, [StringSyntax("route")] string prefix)
{
var api = app.MapGroup(prefix + "/{projectType}/{projectCode}")
- .WithOpenApi(operation =>
+ .AddOpenApiOperationTransformer((operation, _, _) =>
{
- operation.Parameters.Add(new()
+ operation.Parameters?.Add(new OpenApiParameter()
{
Name = "projectType",
In = ParameterLocation.Path,
@@ -42,19 +42,19 @@ public static IEndpointConventionBuilder MapMiniLcmRoutes(this IEndpointRouteBui
{
Enum =
[
- new OpenApiString(ProjectDataFormat.FwData.ToString()),
- new OpenApiString(ProjectDataFormat.Harmony.ToString())
+ JsonValue.Create(ProjectDataFormat.FwData.ToString()),
+ JsonValue.Create(ProjectDataFormat.Harmony.ToString())
],
- Type = "string"
+ Type = JsonSchemaType.String
},
});
- operation.Parameters.Add(new()
+ operation.Parameters?.Add(new OpenApiParameter()
{
Name = "projectCode",
In = ParameterLocation.Path,
Required = true
});
- return operation;
+ return Task.CompletedTask;
})
.AddEndpointFilter(async (context, next) =>
{
diff --git a/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs
index 0795351782..77c3ce0ff0 100644
--- a/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/ProjectRoutes.cs
@@ -18,7 +18,7 @@ public static class ProjectRoutes
{
public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication app)
{
- var group = app.MapGroup("/api").WithOpenApi();
+ var group = app.MapGroup("/api");
group.MapGet("/remoteProjects",
async (CombinedProjectsService combinedProjectsService) =>
{
diff --git a/backend/FwLite/FwLiteWeb/Routes/TestRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/TestRoutes.cs
index cf5abe3268..6a8d186fc3 100644
--- a/backend/FwLite/FwLiteWeb/Routes/TestRoutes.cs
+++ b/backend/FwLite/FwLiteWeb/Routes/TestRoutes.cs
@@ -3,7 +3,7 @@
using LcmCrdt;
using FwLiteWeb.Hubs;
using FwLiteWeb.Services;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using MiniLcm;
using MiniLcm.Models;
@@ -13,15 +13,15 @@ public static class TestRoutes
{
public static IEndpointConventionBuilder MapTest(this WebApplication app)
{
- var group = app.MapGroup("/api/test/{project}").WithOpenApi(operation =>
+ var group = app.MapGroup("/api/test/{project}").AddOpenApiOperationTransformer((operation, _, _) =>
{
- operation.Parameters.Add(new OpenApiParameter()
+ operation.Parameters?.Add(new OpenApiParameter()
{
Name = CrdtMiniLcmApiHub.ProjectRouteKey,
In = ParameterLocation.Path,
Required = true
});
- return operation;
+ return Task.CompletedTask;
});
group.MapGet("/entries",
(IMiniLcmApi api) =>
diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt
index 87c97de674..c4255efd84 100644
--- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt
+++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt
@@ -10,7 +10,6 @@
Keys:
Id PK
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -33,7 +32,6 @@
Keys:
Id PK
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -71,7 +69,6 @@
Annotations:
Relational:Filter: ComponentSenseId IS NOT NULL
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -93,7 +90,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -129,7 +125,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -171,7 +166,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -203,7 +197,6 @@
SenseId
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -236,7 +229,6 @@
Kind Unique
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -259,7 +251,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -281,7 +272,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -305,7 +295,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -331,7 +320,7 @@
SnapshotId (no field, Guid?) Shadow FK Index
Navigations:
ExampleSentences (List) Collection ToDependent ExampleSentence
- PartOfSpeech (PartOfSpeech) ToPrincipal PartOfSpeech
+ PartOfSpeech (PartOfSpeech) Optional ToPrincipal PartOfSpeech
Keys:
Id PK
Foreign keys:
@@ -343,7 +332,6 @@
PartOfSpeechId
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -372,7 +360,6 @@
SnapshotId Unique
WsId, Type Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -404,7 +391,6 @@
Keys:
Id PK
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -424,7 +410,6 @@
Foreign keys:
ChangeEntity {'CommitId'} -> Commit {'Id'} Required Cascade ToDependent: ChangeEntities
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -446,7 +431,7 @@
ElementType: Element type: Guid Required
TypeName (string) Required
Navigations:
- Commit (Commit) ToPrincipal Commit Inverse: Snapshots
+ Commit (Commit) Required ToPrincipal Commit Inverse: Snapshots
Keys:
Id PK
Foreign keys:
@@ -455,7 +440,6 @@
EntityId
CommitId, EntityId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -469,7 +453,6 @@
Keys:
Id PK
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -489,7 +472,6 @@
Indexes:
SnapshotId Unique
Annotations:
- DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
@@ -497,4 +479,4 @@
Relational:ViewName:
Relational:ViewSchema:
Annotations:
- ProductVersion: 9.0.6
\ No newline at end of file
+ ProductVersion: 10.0.7
\ No newline at end of file
diff --git a/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs
index 8d9dbebd73..9804ec65be 100644
--- a/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs
+++ b/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs
@@ -1,6 +1,6 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
-using LinqToDB.Common;
+using LinqToDB.Internal.Common;
using SIL.Harmony;
using SIL.Harmony.Changes;
using SIL.Harmony.Core;
diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
index 5bd78128d0..00efaa550e 100644
--- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
+++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
@@ -10,6 +10,7 @@
using LcmCrdt.MediaServer;
using LcmCrdt.Objects;
using LinqToDB;
+using LinqToDB.Async;
using LinqToDB.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
diff --git a/backend/FwLite/LcmCrdt/Data/EntryQueryHelpers.cs b/backend/FwLite/LcmCrdt/Data/EntryQueryHelpers.cs
index 37eb983a7e..69b7662ffa 100644
--- a/backend/FwLite/LcmCrdt/Data/EntryQueryHelpers.cs
+++ b/backend/FwLite/LcmCrdt/Data/EntryQueryHelpers.cs
@@ -47,11 +47,13 @@ public static bool SearchHeadwords(this Entry e, string? leading, string? traili
private static Expression> SearchHeadwords()
{
+ //Use Sql.Expr to spell the cross-scope path access explicitly: linq2db v6
+ //emits `[kv].*` instead of `[kv].[key]` if we route this through Json.Value.
return (e, leading, trailing, query) =>
Json.QueryValues(e.CitationForm).Any(
v => SqlHelpers.ContainsIgnoreCaseAccents(v, query)) ||
Json.QueryEntries(e.LexemeForm).Any(kv =>
- string.IsNullOrEmpty((Json.Value(e.CitationForm, ms => ms[kv.Key]) ?? "").Trim()) &&
+ string.IsNullOrEmpty((Sql.Expr($"{e.CitationForm}->>{kv.Key}") ?? "").Trim()) &&
SqlHelpers.ContainsIgnoreCaseAccents((leading ?? "") + (kv.Value ?? "").Trim() + (trailing ?? ""), query));
}
diff --git a/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs b/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs
index c1a8cf690a..8b3273b6da 100644
--- a/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs
+++ b/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs
@@ -4,6 +4,7 @@
using LcmCrdt.FullTextSearch;
using LcmCrdt.Utils;
using LinqToDB;
+using LinqToDB.Async;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
diff --git a/backend/FwLite/LcmCrdt/EntryFilterMapProvider.cs b/backend/FwLite/LcmCrdt/EntryFilterMapProvider.cs
index 80b94417da..a4e4905666 100644
--- a/backend/FwLite/LcmCrdt/EntryFilterMapProvider.cs
+++ b/backend/FwLite/LcmCrdt/EntryFilterMapProvider.cs
@@ -7,12 +7,11 @@ public class EntryFilterMapProvider : EntryFilterMapProvider
{
public override Expression> EntrySensesSemanticDomains => e => e.Senses.Select(s => s.SemanticDomains);
public override Expression> EntrySensesSemanticDomainsCode =>
- //ideally we would use Json.Query(s.SemanticDomains) but Gridify doesn't support that, so we have to configure
- //linq2db to rewrite this to that.
- e => e.Senses.SelectMany(s => s.SemanticDomains).Select(sd => Json.Value(sd, sd => sd.Code));
+ //SemanticDomainRows is the json_each rewrite target — see LcmCrdtKernel + LINQ2DB-V6-NOTES.md.
+ e => e.Senses.SelectMany(s => s.SemanticDomainRows).Select(sd => Json.Value(sd, sd => sd.Code));
public override Func? EntrySensesSemanticDomainsConverter =>
- //linq2db treats Sense.SemanticDomains as a table, if we use "null" then it'll write the query we want
- EntryFilter.NormalizeEmptyToNull;
+ //Empty lists are stored as "[]", never SQL NULL — compare against [] like ComplexFormTypes does.
+ EntryFilter.NormalizeEmptyToEmptyList;
public override Expression> EntrySensesExampleSentences => e => e.Senses.Select(s => s.ExampleSentences);
public override Expression> EntrySensesExampleSentencesSentence =>
(e, ws) => e.Senses.SelectMany(s => s.ExampleSentences).Select(example => Json.Value(example.Sentence, ms => ms[ws])!.GetPlainText());
@@ -32,6 +31,6 @@ public class EntryFilterMapProvider : EntryFilterMapProvider
public override Func? EntryComplexFormTypesConverter => EntryFilter.NormalizeEmptyToEmptyList;
public override Expression> EntryPublishIn => e => e.PublishIn;
public override Expression> EntryPublishInId =>
- e => e.PublishIn.Select(p => Json.Value(p, p => p.Id.ToString()));
- public override Func? EntryPublishInConverter => EntryFilter.NormalizeEmptyToNull;
+ e => e.PublishInRows.Select(p => Json.Value(p, p => p.Id.ToString()));
+ public override Func? EntryPublishInConverter => EntryFilter.NormalizeEmptyToEmptyList;
}
diff --git a/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs b/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs
index 7a33e50f2c..b7bbc3ff83 100644
--- a/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs
+++ b/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs
@@ -1,6 +1,7 @@
using System.Text;
using LcmCrdt.Data;
using LinqToDB;
+using LinqToDB.Async;
using LinqToDB.Data;
using LinqToDB.DataProvider.SQLite;
using LinqToDB.EntityFrameworkCore;
@@ -223,16 +224,12 @@ public async Task UpdateEntrySearchTable(Entry entry)
private static async Task InsertOrUpdateEntrySearchRecord(EntrySearchRecord record, ITable table)
{
- await table.InsertOrUpdateAsync(() => new EntrySearchRecord()
- {
- Id = record.Id,
- Headword = record.Headword,
- LexemeForm = record.LexemeForm,
- CitationForm = record.CitationForm,
- Definition = record.Definition,
- Gloss = record.Gloss,
- },
- exiting => new EntrySearchRecord()
+ // Can't use table.InsertOrUpdateAsync here because EntrySearchRecord is a virtual table,
+ // and SQLite doesn't support UPSERT statements on virtual tables. Instead, we have to
+ // use the same DELETE+INSERT approach that Linq2DB 5 used to use (Linq2DB 6 changed this
+ // to a proper UPSERT, which is the correct approach most of the time... except here)
+ await table.DeleteAsync(e => e.Id == record.Id);
+ await table.InsertAsync(() => new EntrySearchRecord()
{
Id = record.Id,
Headword = record.Headword,
@@ -270,6 +267,7 @@ public static async Task UpdateEntrySearchTable(IEnumerable entries,
foreach (var entrySearchRecord in searchRecords)
{
//can't use bulk copy here because that creates duplicate rows
+ //TODO: Replace this with a bulk delete followed by a bulk copy, it should be faster - 2026-05 RM
await InsertOrUpdateEntrySearchRecord(entrySearchRecord, entrySearchRecordsTable);
}
diff --git a/backend/FwLite/LcmCrdt/HistoryService.cs b/backend/FwLite/LcmCrdt/HistoryService.cs
index e730abf7eb..a7fc86a344 100644
--- a/backend/FwLite/LcmCrdt/HistoryService.cs
+++ b/backend/FwLite/LcmCrdt/HistoryService.cs
@@ -7,6 +7,7 @@
using LinqToDB.EntityFrameworkCore;
using System.Text.RegularExpressions;
using MiniLcm.Exceptions;
+using LinqToDB.Async;
namespace LcmCrdt;
@@ -154,7 +155,7 @@ public async Task LoadChangeContext(Guid commitId, int changeInde
}
var affectedEntries = await GetAffectedEntryIds(change)
- .SelectAwait(async entryId => await GetCurrentOrLatestEntry(entryId))
+ .Select(async (Guid entryId, CancellationToken _) => await GetCurrentOrLatestEntry(entryId))
.ToArrayAsync();
return new ChangeContext(change, snapshot, affectedEntries);
diff --git a/backend/FwLite/LcmCrdt/Json.cs b/backend/FwLite/LcmCrdt/Json.cs
index 71dfe76294..bb7190eb8a 100644
--- a/backend/FwLite/LcmCrdt/Json.cs
+++ b/backend/FwLite/LcmCrdt/Json.cs
@@ -3,7 +3,8 @@
using System.Text.Json.Serialization.Metadata;
using LcmCrdt.Changes;
using LinqToDB;
-using LinqToDB.Common;
+using LinqToDB.Internal.Common;
+using LinqToDB.Internal.SqlQuery;
using LinqToDB.Mapping;
using LinqToDB.SqlQuery;
using SIL.Harmony;
@@ -14,7 +15,7 @@ public static class Json
{
sealed class JsonValuePathBuilder : Sql.IExtensionCallBuilder
{
- public void Build(Sql.ISqExtensionBuilder builder)
+ public void Build(Sql.ISqlExtensionBuilder builder)
{
var propExpression = builder.GetExpression(0);
@@ -42,7 +43,7 @@ public void Build(Sql.ISqExtensionBuilder builder)
parameters.Insert(0, propExpression);
- var valueExpression = (ISqlExpression)new SqlExpression(typeof(string),
+ var valueExpression = (ISqlExpression)new SqlExpression(new DbDataType(typeof(string)),
expressionStr,
Precedence.Primary,
parameters.ToArray());
@@ -51,7 +52,12 @@ public void Build(Sql.ISqExtensionBuilder builder)
if (returnType != typeof(string) && returnType != typeof(RichString))//bypass rich string so it can be used with .GetPlainText()
{
- valueExpression = PseudoFunctions.MakeTryConvert(new SqlDataType(new DbDataType(returnType)),
+ //v6 dropped PseudoFunctions.MakeTryConvert; build the SqlFunction directly instead.
+ valueExpression = new SqlFunction(
+ new DbDataType(returnType),
+ PseudoFunctions.TRY_CONVERT,
+ canBeNull: true,
+ new SqlDataType(new DbDataType(returnType)),
new SqlDataType(new DbDataType(typeof(string), DataType.Text)),
valueExpression);
}
@@ -61,7 +67,7 @@ public void Build(Sql.ISqExtensionBuilder builder)
private static void BuildParameterPath(Expression? pathBody,
List parameters,
- Sql.ISqExtensionBuilder builder)
+ Sql.ISqlExtensionBuilder builder)
{
while (pathBody is MemberExpression or MethodCallExpression or UnaryExpression)
{
@@ -86,6 +92,11 @@ private static void BuildParameterPath(Expression? pathBody,
{
pathBody = mce.Object ?? mce.Arguments[0];
}
+ else if (mce.Method.DeclaringType == typeof(Sql) && mce.Method.Name == "Alias")
+ {
+ //v6 wraps [ExpressionMethod] substitutions in Sql.Alias(real, ""); peel it.
+ pathBody = mce.Arguments[0];
+ }
else
{
throw new InvalidOperationException($"Invalid property path for expression {mce}.");
diff --git a/backend/FwLite/LcmCrdt/LINQ2DB-V6-NOTES.md b/backend/FwLite/LcmCrdt/LINQ2DB-V6-NOTES.md
new file mode 100644
index 0000000000..cbcdfb2537
--- /dev/null
+++ b/backend/FwLite/LcmCrdt/LINQ2DB-V6-NOTES.md
@@ -0,0 +1,287 @@
+# Linq2Db v6 — shadow-property workaround in LcmCrdt
+
+Captured during the .NET 10 + Linq2Db 5.4 → 6.2.1 upgrade
+(branch `wip/linq2db-v6-attempts`, PR
+[#2264](https://github.com/sillsdev/languageforge-lexbox/pull/2264)).
+
+This document covers:
+
+1. The current working solution (**TL;DR** + **Files** sections).
+2. Why v6 broke things (**Root cause**).
+3. Everything we tried (**Attempt history**) — kept so the next person walking
+ into this doesn't repeat the dead ends.
+4. What's still worth contributing upstream as community-benefit fixes
+ (**Upstream plan**) — lexbox no longer blocks on these.
+
+---
+
+## TL;DR
+
+Two `IList` jsonb columns — `Sense.SemanticDomains` and `Entry.PublishIn` —
+need a `json_each(...)` rewrite for queries (`Any`, `SelectMany`, etc.) but
+*not* at entity materialization. v5 honored that split via
+`ExpressionMethodAttribute(IsColumn = false)`. v6 ignores `IsColumn` and fires
+the substitution at materialization too, which either casts wrong
+(`EnumerableQuery` → `IList`) or invokes a `Sql.TableFunction` body
+client-side.
+
+The fix here:
+
+- Add **non-column shadow properties** `Sense.SemanticDomainRows` and
+ `Entry.PublishInRows` in `MiniLcm/Models/`. They return the underlying list
+ in client context (so reflection-based deep equality and bulk-copy paths
+ don't trip) and have no column mapping.
+- In `LcmCrdtKernel`, attach the `[ExpressionMethod]` rewrite to those shadow
+ properties via `FluentMappingBuilder.IsExpression(..., isColumn: false)`.
+ `IsExpression` also calls `IsNotColumn()`, so BulkCopy and insert paths
+ don't read them.
+- Route Gridify filter projections through the shadow properties
+ (`EntryFilterMapProvider.EntryPublishInId`,
+ `EntrySensesSemanticDomainsCode`).
+- The materialization expression doesn't reference the shadow properties, so
+ v6's `ExposeExpressionVisitor` never sees them — the substitution fires
+ only when LINQ translation hits `e.PublishInRows` in a query.
+
+Result: all of `LcmCrdt.Tests` passes, including the perf assertion and the
+nine filter shapes that the earlier `.ToList()` workaround left red.
+
+---
+
+## Root cause — what changed in v6
+
+v6 rewrote the query parser. The
+[migration wiki](https://github.com/linq2db/linq2db/wiki/Linq-To-DB-6)
+makes one statement that explains both regressions:
+
+> *"For final query projection, linq2db doesn't try to translate it to SQL
+> anymore and sets its value on the client during materialization."*
+
+Paired with a new "single-query preamble" strategy for eager loading
+([`ExpressionBuilder.EagerLoad.cs`](https://github.com/linq2db/linq2db/blob/master/Source/LinqToDB/Internal/Linq/Builder/ExpressionBuilder.EagerLoad.cs)),
+this means an `ExpressionMethodAttribute` registered on a property now fires
+in two places where v5 only fired it in one:
+
+1. **Query translation** (where we *want* it) — `s.SemanticDomains` becomes
+ `Json.Query(Sql.Property<...>(s, "SemanticDomains"))`, which
+ `[Sql.TableFunction("json_each", argIndices: [0])]` then turns into a
+ `json_each(jsonb_column)` table subquery.
+2. **Materialization** (where v5 didn't apply it) — after the row is read and
+ EF's value converter has deserialized the jsonb column to
+ `IList`, v6 *also* runs the substitution lambda
+ client-side over the deserialized list and assigns the result back into
+ the property. This is what causes the regressions.
+
+`IsColumn` on `ExpressionMethodAttribute` is documented to control exactly
+this behavior (`IsColumn=false` should mean "not used during
+materialization"). Verified locally that v6 ignores it: `TableBuilder.TableContext.MakeExpression`
+now passes `fullEntity` through `Builder.ConvertExpressionTree`, which routes
+through `ExposeExpressionVisitor` and expands every `[ExpressionMethod]`
+regardless of `IsColumn`.
+
+### The two original regressions
+
+**Regression 1 — `LoadWith` materialization.** With the rewrite on the
+property and returning `IQueryable` (natural shape — `json_each` is a
+table), v6's materializer evaluates the substitution client-side and throws
+`InvalidCastException : Unable to cast EnumerableQuery to IList` inside
+`LinqToDB.Internal.Linq.QueryRunner.Mapper.ReMapOnException`. Originally
+broke ~280 of 461 tests.
+
+**Regression 2 — translator can't see through `.ToList()`.** Forcing the
+substitution to return `IList` via trailing `.ToList()` fixes
+materialization but defeats SQL translation: filter chains like
+`e.Senses.Any(s => s.SemanticDomains.Any(sd => sd.Code.Contains("Fruit")))`
+expand to
+`...json_each(s.SemanticDomains).Select(v => v.Value).ToList().Any(sd => ...)`,
+and v6 gives up with `LinqToDBException : The LINQ expression could not be
+converted to SQL`.
+
+---
+
+## Attempt history
+
+| # | Approach | Eager load (`LoadWith`) | Gridify content filter | Gridify "missing"/null filter |
+|---|---|---|---|---|
+| A | Drop `[ExpressionMethod]`, write `Json.Query(...)` explicitly in `EntryFilterMapProvider` | ✓ works | ✗ Gridify can't parse `Json.Query(...)` — `InvalidOperationException` at `ParseMethodCallExpression` | ✗ same |
+| B | Keep `[ExpressionMethod]`, expression returns `IQueryable` (no `.ToList()`) | ✗ `InvalidCastException` on every load (~280 fails) | ✓ would translate | n/a (load already broken) |
+| C | Keep `[ExpressionMethod]`, expression returns `IList` via `.ToList()` | ✓ | ✗ translator can't see through `.ToList()` | ✗ same |
+| D | C + `IsColumn = false` explicitly | same as C | same as C | same as C — v6 ignores it |
+| E1 | Shadow **extension method** `e.PublishInAsRows()` with `[ExpressionMethod]` | ✓ | ✗ Gridify's `ParseMethodCallExpression` only handles `MemberExpression` / `Select` / `SelectMany` / `Where` as chain root; a method-call root throws | ✓ (combined with converter change — see below) |
+| **F** | **Shadow *property*** `e.PublishInRows` with `IsExpression(..., isColumn:false)` *(current)* | ✓ | ✓ | ✓ |
+
+### Why the shadow approach works as a property but not a method
+
+Gridify's `LinqQueryBuilder.ParseMethodCallExpression` switches on the first
+argument of the `Select(...)` projection. Only four shapes match:
+`MemberExpression`, or a nested `Select` / `SelectMany` / `Where`
+`MethodCallExpression`. A user-defined extension call like
+`e.PublishInAsRows()` matches none and throws `InvalidOperationException`. A
+property access (`e.PublishInRows`) is a `MemberExpression`, so it does
+match.
+
+### Why v6's materializer doesn't trip on the shadow
+
+The shadow property is unmapped (`[NotMapped, JsonIgnore]`, plus
+`IsNotColumn()` via `IsExpression`). v6's materialization expression is
+shaped like `new Entry { Col1 = ..., Col2 = ... }` over the mapped columns
+only. `ExposeExpressionVisitor` only expands `[ExpressionMethod]` when it
+actually walks past that member in some expression tree. The shadow never
+appears in the materialization tree, so its substitution never fires there.
+
+### Other untried alternatives (still escalation paths if F ever breaks)
+
+In increasing invasiveness:
+
+- Change `Sense.SemanticDomains` / `Entry.PublishIn` from `IList` to
+ `IQueryable` at the model level. Removes the type mismatch but is a wide
+ breaking change in `MiniLcm`.
+- Drop the json column entirely and model these as real one-to-many EF
+ associations. No more `[Sql.TableFunction]` rewriting at all. Schema +
+ migration + sync-format break.
+- Pin `linq2db` to 5.4.x. Avoids the regression at the cost of every fix in
+ the v6 line; conflicts with the rest of the .NET 10 upgrade.
+
+---
+
+## Filter-converter changes
+
+Without the property-level rewrite, `e.PublishIn == null` no longer maps to
+"NOT EXISTS json_each(...)" — it lowers to plain column `IS NULL`, which
+matches nothing because empty lists are stored as `"[]"`, never SQL NULL.
+The two affected converters
+(`EntryPublishInConverter`, `EntrySensesSemanticDomainsConverter`) therefore
+use `NormalizeEmptyToEmptyList` instead of `NormalizeEmptyToNull`, generating
+`column = '[]'` — the same pattern `EntryComplexFormTypesConverter` already
+used.
+
+---
+
+## Two related v6 fixes that landed in `Json.cs`
+
+| Concern | Where |
+|---|---|
+| Peel `Sql.Alias(...)` wrap from `IExtensionCallBuilder` lambda arg bodies | `JsonValuePathBuilder.BuildParameterPath` |
+| `PseudoFunctions.MakeTryConvert` was dropped in v6 — build the `SqlFunction` directly | `JsonValuePathBuilder.Build` |
+
+The Alias-peel exists because `ExposeExpressionVisitor` wraps every
+`[ExpressionMethod]` substitution in `Sql.Alias(real_expr, "")` as a
+column-alias hint, and that wrap leaks into user-written
+`IExtensionCallBuilder` arg lambdas. `Json.Value(p, p => p.Id.ToString())` is
+what trips it in our code; the same shape would affect any other linq2db
+user with `[ExpressionMethod]` + custom extension builders.
+
+---
+
+## Upstream plan (community-benefit, not lexbox-blocking)
+
+With the shadow-property approach in place, lexbox no longer depends on any
+linq2db upstream fix. The two items below are still worth filing as
+community contributions — other users will hit them — but they're off our
+critical path.
+
+### PR A — Honor `IsColumn=false` at entity materialization
+
+The clean win. Restores the documented `[ExpressionMethod].IsColumn`
+contract.
+
+- **Doc contract:** `IsColumn`'s XML doc reads: *"When applied to property
+ and set to true, Linq To DB will load data into property using expression
+ during entity materialization."* The default (`false`) should opt out of
+ materialization-time invocation.
+- **Where it broke:** `TableBuilder.TableContext.MakeExpression` now passes
+ `fullEntity` through `Builder.ConvertExpressionTree`, which routes through
+ `ExposeExpressionVisitor` and expands every `[ExpressionMethod]`
+ regardless of `IsColumn`.
+- **Proposed fix:** thread a `calculatedColumnsOnly` flag through one
+ materialization call site. `ExposeExpressionVisitor.ConvertExpressionMethodAttribute`
+ returns `null` when `_calculatedColumnsOnly && !attr.IsColumn`. Caller
+ falls back to bare member access.
+- **Sample repro:** a `Foo` with `IList Bars` carrying a `JsonEach`-style
+ substitution and `IsColumn = false` — `db.GetTable().ToList()` throws
+ in v6 but works in v5.
+- **Risk profile:** low. Anyone relying on v6's regression behavior would
+ have been broken on v5 too; they should set `IsColumn=true`.
+
+### PR C — Peel `Sql.Alias` from `IExtensionCallBuilder` lambda args
+
+Narrow, surface-area-only fix. No public contract change.
+
+- **Symptom:** `ExposeExpressionVisitor` wraps every `[ExpressionMethod]`
+ substitution in `Sql.Alias(real_expr, "")`. The wrap is
+ invisible to the SQL builder but observable inside user-written
+ `Sql.IExtensionCallBuilder` lambda-arg walkers as `Alias(real_expr, "ToString")`.
+- **Proposed fix:** in `Sql.ExtensionAttribute.GetExtensionParam`, peel
+ `Sql.Alias(...)` from lambda-argument bodies before invoking the user
+ builder. Top-level (non-lambda) argument expressions are left alone.
+- **Lexbox-side workaround already in `Json.cs`** — see the table above.
+- **Risk profile:** low. The wrap was never part of the documented
+ extension-builder contract.
+
+### PR B — retired
+
+Previously envisioned as "suppress collection-typed `[ExpressionMethod]`
+substitution inside `Equal`/`NotEqual` against null" so that
+`e.PublishIn == null` would lower to column `IS NULL`. The shadow-property
+approach makes this moot — the bare null check now lands on the column
+naturally, no engine change needed. Don't open this PR.
+
+### Sequencing
+
+A → C, parallelizable. Cadence on `linq2db/linq2db` (sampled 2026-05-18):
+~10 PRs/week merged, **6.3.0 shipped 2026-05-17**, **6.4.0 version bump
+merged 2026-05-18**. Small bug-fixes with a linked issue land same-day to
+3-day; medium fixes 1–2 weeks. Issue-first is convention; PR titles read
+`Fix #NNNN: `. Maintainers worth tagging: `MaceWindu` (release
+management + cross-provider review), `igor-tkachev` (original author),
+`sdanyliv` (most active feature contributor).
+
+If we file A + C, realistic landing window is 1–2 weeks each, with a shot
+at the 6.4.x line.
+
+---
+
+## Files
+
+- `MiniLcm/Models/Entry.cs`, `MiniLcm/Models/Sense.cs` — shadow properties.
+- `LcmCrdt/LcmCrdtKernel.cs` — `IsExpression` registration and the rewrite
+ factories (`SenseSemanticDomainRowsExpression`,
+ `EntryPublishInRowsExpression`).
+- `LcmCrdt/Json.cs` — `Sql.Alias` peel; `TRY_CONVERT` direct construction.
+- `LcmCrdt/EntryFilterMapProvider.cs` — projections routed through shadow
+ properties; converters use `NormalizeEmptyToEmptyList`.
+- `LcmCrdt.Tests/MiniLcmTests/QueryEntryTests.cs:79` — perf-test assertion
+ margin (history of past adjustments in comments above the line).
+
+---
+
+## Trade-offs
+
+- The shadow accessor is a soft lie in client context — anyone calling
+ `entry.PublishInRows` from non-query code gets the underlying list, not a
+ `json_each` table. Surface area is tiny (two properties, marked
+ `[NotMapped, JsonIgnore]`) and the comment on each points back here.
+- The shape leaks a query-engine concern into `MiniLcm`'s shared domain
+ model. Acceptable cost given the alternative (waiting on PR A or shipping
+ the `.ToList()` workaround with 9 red tests).
+- If PR A lands and we upgrade to a fixed linq2db release, the shadow
+ properties can be collapsed back into bare `[ExpressionMethod]` on the
+ underlying columns. Not load-bearing for lexbox.
+
+---
+
+## Links
+
+- Linq To DB 6 migration guide:
+
+- Eager-load refactor in flight (targets 6.4.0) — may fix or change this:
+
+- `ExpressionBuilder.EagerLoad.cs` (the file the stack trace points into):
+
+- Related (similar v6 `[ExpressionMethod]` regressions, all fixed — none
+ match our pair of symptoms but useful as precedent):
+ -
+ -
+ -
+ -
+- Local upstream-side scratch (PR drafts, repro shapes for A and C):
+ `D:\code\linq2db\UPSTREAM-PLAN.md` (outside this repo).
diff --git a/backend/FwLite/LcmCrdt/LcmCrdt.csproj b/backend/FwLite/LcmCrdt/LcmCrdt.csproj
index df5353a90a..f8cfb1db27 100644
--- a/backend/FwLite/LcmCrdt/LcmCrdt.csproj
+++ b/backend/FwLite/LcmCrdt/LcmCrdt.csproj
@@ -8,8 +8,8 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
index 9d98c1ebbf..1ab81cf990 100644
--- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
+++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
@@ -12,9 +12,9 @@
using LcmCrdt.Objects;
using LcmCrdt.RemoteSync;
using LinqToDB;
-using LinqToDB.AspNet.Logging;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore;
+using LinqToDB.Extensions.Logging;
using LinqToDB.Mapping;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
@@ -130,16 +130,19 @@ public static void ConfigureDbOptions(IServiceProvider provider, DbContextOption
nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.DateTime)))
.HasAttribute(new ColumnAttribute(nameof(HybridDateTime.Counter),
nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter)))
- //tells linq2db to rewrite Sense.SemanticDomains, into Json.Query(Sense.SemanticDomains)
- .Entity().Property(s => s.SemanticDomains).HasAttribute(new ExpressionMethodAttribute(SenseSemanticDomainsExpression()))
- .Entity().Property(e => e.PublishIn).HasAttribute(new ExpressionMethodAttribute(EntryPublishInExpression()))
+ //Lower the shadow properties to json_each form. The rewrite lives on these
+ //unmapped accessors instead of on the underlying IList columns because v6's
+ //materializer expands [ExpressionMethod] on mapped properties regardless of
+ //IsColumn=false (see LINQ2DB-V6-NOTES.md).
+ .Entity().Property(s => s.SemanticDomainRows).IsExpression(SenseSemanticDomainRowsExpression(), isColumn: false)
+ .Entity().Property(e => e.PublishInRows).IsExpression(EntryPublishInRowsExpression(), isColumn: false)
.Entity().Member(r => r.GetPlainText()).IsExpression(r => Json.GetPlainText(r))
.Entity().Member(g => g.ToString()).IsExpression(g => Json.ToString(g))
.Build();
mappingSchema.SetConvertExpression((WritingSystemId id) =>
new DataParameter { Value = id.Code, DataType = DataType.Text });
optionsBuilder.AddMappingSchema(mappingSchema);
- optionsBuilder.AddCustomOptions(options => options.UseSQLiteMicrosoft());
+ optionsBuilder.AddCustomOptions(options => options.UseSQLite());
// Register read-relevant interceptors for LinqToDB
var sqliteFunctionInterceptor = new CustomSqliteFunctionInterceptor();
@@ -161,18 +164,13 @@ public static void ConfigureDbOptions(IServiceProvider provider, DbContextOption
builder.AddInterceptors(updateSearchTableInterceptor);
}
- private static Expression>> SenseSemanticDomainsExpression()
- {
- //using Sql.Property, otherwise if we used `s.SemanticDomains` again it would be recursively rewritten
- return s => Json.Query(Sql.Property>(s, nameof(Sense.SemanticDomains)));
- }
-
- private static Expression>> EntryPublishInExpression()
- {
- //using Sql.Property, otherwise if we used `e.PublishIn` again it would be recursively rewritten
- return e => Json.Query(Sql.Property>(e, nameof(Entry.PublishIn)));
- }
+ //Sql.Property (not bare s.SemanticDomains) so the substitution doesn't recurse if the
+ //underlying column ever picks up an [ExpressionMethod] of its own.
+ private static Expression>> SenseSemanticDomainRowsExpression()
+ => s => Json.Query(Sql.Property>(s, nameof(Sense.SemanticDomains)));
+ private static Expression>> EntryPublishInRowsExpression()
+ => e => Json.Query(Sql.Property>(e, nameof(Entry.PublishIn)));
public static void ConfigureCrdt(CrdtConfig config)
{
@@ -263,7 +261,7 @@ public static void ConfigureCrdt(CrdtConfig config)
list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
json => JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null) ?? Array.Empty());
- var writingSystemArrayConverter = new ValueConverter(
+ var writingSystemArrayConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter(
list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
json => json == null ? null : JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null));
builder.Property(v => v.Vernacular)
diff --git a/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs b/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
index 5cddad6fec..dd0f0974a9 100644
--- a/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
+++ b/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
@@ -16,7 +16,7 @@ partial class LcmCrdtDbContextModelSnapshot : ModelSnapshot
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("LcmCrdt.FullTextSearch.EntrySearchRecord", b =>
{
@@ -89,7 +89,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasKey("Id");
- b.ToTable("ProjectData");
+ b.ToTable("ProjectData", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.ComplexFormComponent", b =>
@@ -164,7 +164,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("ComplexFormType");
+ b.ToTable("ComplexFormType", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.CustomView", b =>
@@ -209,7 +209,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("CustomView");
+ b.ToTable("CustomView", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.Entry", b =>
@@ -256,7 +256,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("Entry");
+ b.ToTable("Entry", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.ExampleSentence", b =>
@@ -295,7 +295,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("ExampleSentence");
+ b.ToTable("ExampleSentence", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.MorphType", b =>
@@ -342,7 +342,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("MorphType");
+ b.ToTable("MorphType", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.PartOfSpeech", b =>
@@ -369,7 +369,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("PartOfSpeech");
+ b.ToTable("PartOfSpeech", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.Publication", b =>
@@ -393,7 +393,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("Publication");
+ b.ToTable("Publication", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.SemanticDomain", b =>
@@ -424,7 +424,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("SemanticDomain");
+ b.ToTable("SemanticDomain", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.Sense", b =>
@@ -469,7 +469,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("Sense");
+ b.ToTable("Sense", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.WritingSystem", b =>
@@ -518,7 +518,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("WsId", "Type")
.IsUnique();
- b.ToTable("WritingSystem");
+ b.ToTable("WritingSystem", (string)null);
});
modelBuilder.Entity("SIL.Harmony.Commit", b =>
@@ -542,7 +542,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("TEXT");
- b.ComplexProperty>("HybridDateTime", "SIL.Harmony.Commit.HybridDateTime#HybridDateTime", b1 =>
+ b.ComplexProperty(typeof(Dictionary), "HybridDateTime", "SIL.Harmony.Commit.HybridDateTime#HybridDateTime", b1 =>
{
b1.IsRequired();
@@ -631,7 +631,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasKey("Id");
- b.ToTable("LocalResource");
+ b.ToTable("LocalResource", (string)null);
});
modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b =>
@@ -654,7 +654,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("SnapshotId")
.IsUnique();
- b.ToTable("RemoteResource");
+ b.ToTable("RemoteResource", (string)null);
});
modelBuilder.Entity("MiniLcm.Models.ComplexFormComponent", b =>
diff --git a/backend/FwLite/LcmCrdt/Objects/DbTranslationDeserializationTarget.cs b/backend/FwLite/LcmCrdt/Objects/DbTranslationDeserializationTarget.cs
index a249a5cd10..de5a947a27 100644
--- a/backend/FwLite/LcmCrdt/Objects/DbTranslationDeserializationTarget.cs
+++ b/backend/FwLite/LcmCrdt/Objects/DbTranslationDeserializationTarget.cs
@@ -1,6 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-using LinqToDB.Common;
+using LinqToDB.Internal.Common;
namespace LcmCrdt.Objects;
diff --git a/backend/FwLite/MiniLcm.Tests/FluentAssertGlobalConfig.cs b/backend/FwLite/MiniLcm.Tests/FluentAssertGlobalConfig.cs
index 3cfb18764d..26d04eb4d2 100644
--- a/backend/FwLite/MiniLcm.Tests/FluentAssertGlobalConfig.cs
+++ b/backend/FwLite/MiniLcm.Tests/FluentAssertGlobalConfig.cs
@@ -1,3 +1,5 @@
+using FluentAssertions;
+using FluentAssertions.Equivalency;
using FluentAssertions.Extensibility;
using MiniLcm.Tests;
@@ -16,6 +18,9 @@ public static void Initialize()
.ComparingByMembers()
.Excluding(m => (m.DeclaringType == typeof(ComplexFormComponent) || m.DeclaringType == typeof(WritingSystem))
&& (m.Name == nameof(ComplexFormComponent.Id) || m.Name == nameof(ComplexFormComponent.MaybeId)))
+ //Shadow query-rewrite targets — domain state lives on the underlying collection.
+ .Excluding(m => (m.DeclaringType == typeof(Entry) && m.Name == nameof(Entry.PublishInRows))
+ || (m.DeclaringType == typeof(Sense) && m.Name == nameof(Sense.SemanticDomainRows)))
);
}
}
diff --git a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs
index de2f12728f..830bea2023 100644
--- a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs
+++ b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs
@@ -270,7 +270,7 @@ public async Task UpdateEntry_CanReorderComponents(string before, string after,
var entryId = Guid.NewGuid();
var componentHeadwordsToIds = before.Split(',').Concat(after.Split(',')).Distinct()
.ToDictionary(i => i, _ => Guid.NewGuid());
- var componentHeadwordsToEntryIds = componentHeadwordsToIds.Keys.ToAsyncEnumerable().SelectAwait(async @char =>
+ var componentHeadwordsToEntryIds = componentHeadwordsToIds.Keys.ToAsyncEnumerable().Select(async (string @char, CancellationToken _) =>
{
var componentEntry = await Api.CreateEntry(new()
{
diff --git a/backend/FwLite/MiniLcm.Tests/Validators/MorphTypeUpdateValidatorTests.cs b/backend/FwLite/MiniLcm.Tests/Validators/MorphTypeUpdateValidatorTests.cs
index d07a4004a2..c0c42b7fe6 100644
--- a/backend/FwLite/MiniLcm.Tests/Validators/MorphTypeUpdateValidatorTests.cs
+++ b/backend/FwLite/MiniLcm.Tests/Validators/MorphTypeUpdateValidatorTests.cs
@@ -41,7 +41,7 @@ public void Fails_WhenTryingToUpdateDeletedAt()
public void Fails_WhenThereAreNoChanges()
{
var update = NewUpdate();
- _validator.TestValidate(update).ShouldHaveAnyValidationError();
+ _validator.TestValidate(update).ShouldHaveValidationErrors();
}
[Fact]
diff --git a/backend/FwLite/MiniLcm.Tests/Validators/WritingSystemUpdateValidatorTests.cs b/backend/FwLite/MiniLcm.Tests/Validators/WritingSystemUpdateValidatorTests.cs
index 854769f09b..94bb714c8e 100644
--- a/backend/FwLite/MiniLcm.Tests/Validators/WritingSystemUpdateValidatorTests.cs
+++ b/backend/FwLite/MiniLcm.Tests/Validators/WritingSystemUpdateValidatorTests.cs
@@ -40,6 +40,6 @@ public void Fails_WhenTryingToUpdateType()
public void Fails_WhenThereAreNoChanges()
{
var update = NewUpdate();
- _validator.TestValidate(update).ShouldHaveAnyValidationError();
+ _validator.TestValidate(update).ShouldHaveValidationErrors();
}
}
diff --git a/backend/FwLite/MiniLcm/MiniLcm.csproj b/backend/FwLite/MiniLcm/MiniLcm.csproj
index d8b55b7497..216e3df2b4 100644
--- a/backend/FwLite/MiniLcm/MiniLcm.csproj
+++ b/backend/FwLite/MiniLcm/MiniLcm.csproj
@@ -9,7 +9,6 @@
-
diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs
index 550080e628..1871ea4dad 100644
--- a/backend/FwLite/MiniLcm/Models/Entry.cs
+++ b/backend/FwLite/MiniLcm/Models/Entry.cs
@@ -1,3 +1,8 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using MiniLcm.Attributes;
+
namespace MiniLcm.Models;
public record Entry : IObjectWithId
@@ -29,6 +34,13 @@ public record Entry : IObjectWithId
public virtual List PublishIn { get; set; } = [];
+ //Server-side query rewrite target — LcmCrdt lowers this to json_each(PublishIn) at query time.
+ //Public only because the LcmCrdt filter map provider lives in a different assembly; treat as
+ //internal. Hidden from IntelliSense, JSON, EF column mapping, and FA equivalency.
+ //See LINQ2DB-V6-NOTES.md.
+ [MiniLcmInternal, NotMapped, JsonIgnore, EditorBrowsable(EditorBrowsableState.Never)]
+ public IEnumerable PublishInRows => PublishIn;
+
public const string UnknownHeadword = "(Unknown)";
public string Headword()
diff --git a/backend/FwLite/MiniLcm/Models/RichMultiString.cs b/backend/FwLite/MiniLcm/Models/RichMultiString.cs
index eeac03a25b..9cd98cf29f 100644
--- a/backend/FwLite/MiniLcm/Models/RichMultiString.cs
+++ b/backend/FwLite/MiniLcm/Models/RichMultiString.cs
@@ -38,9 +38,14 @@ public RichMultiString Copy()
void IDictionary.Add(object key, object? value)
{
- var valStr = value as RichString ??
- throw new ArgumentException($"unable to convert value {value?.GetType().Name ?? "null"} to RichString",
- nameof(value));
+ //SystemTextJsonPatch v5 hands us the raw JsonElement; deserialize so PocoAdapter callers work.
+ var valStr = value switch
+ {
+ RichString rs => rs,
+ JsonElement je => je.Deserialize()
+ ?? throw new ArgumentException("unable to deserialize JsonElement to RichString", nameof(value)),
+ _ => throw new ArgumentException($"unable to convert value {value?.GetType().Name ?? "null"} to RichString", nameof(value))
+ };
Add(WritingSystemId.FromUnknown(key), valStr);
}
diff --git a/backend/FwLite/MiniLcm/Models/Sense.cs b/backend/FwLite/MiniLcm/Models/Sense.cs
index 3c4016f13e..b4938dc867 100644
--- a/backend/FwLite/MiniLcm/Models/Sense.cs
+++ b/backend/FwLite/MiniLcm/Models/Sense.cs
@@ -1,3 +1,5 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using System.Text.Json.Serialization;
using MiniLcm.Attributes;
@@ -18,6 +20,10 @@ public class Sense : IObjectWithId, IOrderable
public virtual PartOfSpeech? PartOfSpeech { get; set; } = null;
public virtual Guid? PartOfSpeechId { get; set; }
public virtual IList SemanticDomains { get; set; } = [];
+
+ //Server-side query rewrite target — see Entry.PublishInRows.
+ [MiniLcmInternal, NotMapped, JsonIgnore, EditorBrowsable(EditorBrowsableState.Never)]
+ public IEnumerable SemanticDomainRows => SemanticDomains;
public virtual List ExampleSentences { get; set; } = [];
public Guid[] GetReferences()
diff --git a/backend/LexBoxApi/Auth/Attributes/LexboxAuthAttribute.cs b/backend/LexBoxApi/Auth/Attributes/LexboxAuthAttribute.cs
index edf8c7ef19..f5cf679b32 100644
--- a/backend/LexBoxApi/Auth/Attributes/LexboxAuthAttribute.cs
+++ b/backend/LexBoxApi/Auth/Attributes/LexboxAuthAttribute.cs
@@ -28,8 +28,8 @@ public string Policy
protected override void TryConfigure(IDescriptorContext context,
IDescriptor descriptor,
- ICustomAttributeProvider element)
+ ICustomAttributeProvider? element)
{
- ApplyAttribute(context, descriptor, element, new AuthorizeAttribute(Policy));
+ if (element is not null) ApplyAttribute(context, descriptor, element, new AuthorizeAttribute(Policy));
}
}
diff --git a/backend/LexBoxApi/Auth/AuthKernel.cs b/backend/LexBoxApi/Auth/AuthKernel.cs
index d7955a907d..d26ca94c46 100644
--- a/backend/LexBoxApi/Auth/AuthKernel.cs
+++ b/backend/LexBoxApi/Auth/AuthKernel.cs
@@ -16,7 +16,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Logging;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using OpenIddict.Core;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
@@ -242,16 +242,11 @@ public static void AddLexBoxAuth(IServiceCollection services,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
- options.AddSecurityRequirement(new OpenApiSecurityRequirement
+ options.AddSecurityRequirement(_ => new OpenApiSecurityRequirement
{
{
- new OpenApiSecurityScheme
- {
- Name = "Bearer",
- In = ParameterLocation.Header,
- Reference = new OpenApiReference { Id = "Bearer", Type = ReferenceType.SecurityScheme }
- },
- new List()
+ new OpenApiSecuritySchemeReference("Bearer"),
+ []
}
});
});
diff --git a/backend/LexBoxApi/GraphQL/CustomTypes/IsLanguageForgeProjectDataLoader.cs b/backend/LexBoxApi/GraphQL/CustomTypes/IsLanguageForgeProjectDataLoader.cs
index 59b9c04697..3350687e0f 100644
--- a/backend/LexBoxApi/GraphQL/CustomTypes/IsLanguageForgeProjectDataLoader.cs
+++ b/backend/LexBoxApi/GraphQL/CustomTypes/IsLanguageForgeProjectDataLoader.cs
@@ -1,5 +1,6 @@
using LexCore.ServiceInterfaces;
using LfClassicData;
+using LfClassicData.Entities;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Polly;
@@ -58,12 +59,11 @@ private static async Task> LoadBatch(IsLanguageForgePro
IReadOnlyList list,
CancellationToken token)
{
- var actualProjects = await MongoExtensions.ToAsyncEnumerable(loader._systemDbContext.Projects.AsQueryable()
- .Select(p => p.ProjectCode)
-#pragma warning disable MALinq2001
- .Where(projectCode => list.Contains(projectCode)))
-#pragma warning restore MALinq2001
- .ToHashSetAsync(cancellationToken: token);
+ var actualProjects = (await loader._systemDbContext.Projects
+ .Find(Builders.Filter.In(p => p.ProjectCode, list))
+ .Project(p => p.ProjectCode)
+ .ToListAsync(cancellationToken: token)
+ ).ToHashSet();
return list.ToDictionary(pc => pc, pc => actualProjects.Contains(pc));
}
diff --git a/backend/LexBoxApi/LexBoxApi.csproj b/backend/LexBoxApi/LexBoxApi.csproj
index e30e232194..f290067aa0 100644
--- a/backend/LexBoxApi/LexBoxApi.csproj
+++ b/backend/LexBoxApi/LexBoxApi.csproj
@@ -1,7 +1,6 @@
Linux
- true
7392cddf-9b3b-441c-9316-203bb5c4a6bc
1
@@ -50,7 +49,6 @@
-
diff --git a/backend/LexBoxApi/Program.cs b/backend/LexBoxApi/Program.cs
index 9898ef873d..dafe074a2f 100644
--- a/backend/LexBoxApi/Program.cs
+++ b/backend/LexBoxApi/Program.cs
@@ -21,9 +21,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using tusdotnet;
-using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
if (MigrationKernel.IsMigrationRequest(args))
{
@@ -140,7 +139,7 @@
foreach (var knownNetwork in configuration.GetSection("ForwardedHeadersOptions:KnownNetworks").GetChildren())
{
- options.KnownNetworks.Add(IPNetwork.Parse(knownNetwork.Value!));
+ options.KnownIPNetworks.Add(IPNetwork.Parse(knownNetwork.Value!));
}
});
@@ -186,7 +185,7 @@
app.MapQuartzUI("/api/quartz").RequireAuthorization(new AdminRequiredAttribute());
app.MapControllers();
-app.MapLfClassicApi().WithOpenApi().WithGroupName(LexBoxKernel.OpenApiPublicDocumentName)
+app.MapLfClassicApi().WithGroupName(LexBoxKernel.OpenApiPublicDocumentName)
.RequireAuthorization(policyBuilder => policyBuilder.RequireAuthenticatedUser().AddRequirements(new UserHasAccessToProjectRequirement()));
app.MapTus("/api/tus-test",
async context => await context.RequestServices.GetRequiredService().GetTestConfig(context))
diff --git a/backend/LexBoxApi/Proxies/FileUploadProxy.cs b/backend/LexBoxApi/Proxies/FileUploadProxy.cs
index fbfd691dbf..bc5839961f 100644
--- a/backend/LexBoxApi/Proxies/FileUploadProxy.cs
+++ b/backend/LexBoxApi/Proxies/FileUploadProxy.cs
@@ -36,7 +36,7 @@ public static IEndpointConventionBuilder MapFileUploadProxy(this IEndpointRouteB
Policy = UserCanDownloadMediaFilesPolicy
};
- var group = app.MapGroup("/api/media").WithOpenApi();
+ var group = app.MapGroup("/api/media");
//media upload/download
group.Map("/list/{projectId:guid}/{**catch-all}",
diff --git a/backend/LexBoxApi/Services/CrdtCommitService.cs b/backend/LexBoxApi/Services/CrdtCommitService.cs
index eed0dcc33e..d4ec51f8c9 100644
--- a/backend/LexBoxApi/Services/CrdtCommitService.cs
+++ b/backend/LexBoxApi/Services/CrdtCommitService.cs
@@ -1,6 +1,7 @@
using LexCore.Utils;
using LexData;
using LinqToDB;
+using LinqToDB.Async;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.EntityFrameworkCore.Internal;
@@ -15,26 +16,18 @@ public async Task AddCommits(Guid projectId, IAsyncEnumerable comm
await using var transaction = await dbContext.Database.BeginTransactionAsync(token);
var linqToDbContext = dbContext.CreateLinqToDBContext();
await using var tmpTable = await linqToDbContext.CreateTempTableAsync($"tmp_crdt_commit_import_{projectId}__{Guid.NewGuid()}", cancellationToken: token);
- await tmpTable.BulkCopyAsync(new BulkCopyOptions{BulkCopyType = BulkCopyType.ProviderSpecific, MaxBatchSize = 10}, commits, token);
+ //Stamp ProjectId while streaming so the merge below can be a plain column-to-column copy.
+ //A projection lambda here would let linq2db v6 wrap our Sql.Expr<...>::jsonb cast in the
+ //EF value-converter (JsonSerializer.Serialize) and fail SQL translation.
+ var stampedCommits = commits.Select(c => { c.ProjectId = projectId; return c; });
+ await tmpTable.BulkCopyAsync(new BulkCopyOptions{BulkCopyType = BulkCopyType.ProviderSpecific, MaxBatchSize = 10}, stampedCommits, token);
var commitsTable = linqToDbContext.GetTable();
await commitsTable
.Merge()
.Using(tmpTable)
.OnTargetKey()
- .InsertWhenNotMatched(commit => new ServerCommit(commit.Id)
- {
- Id = commit.Id,
- ClientId = commit.ClientId,
- HybridDateTime = new HybridDateTime(commit.HybridDateTime.DateTime, commit.HybridDateTime.Counter)
- {
- DateTime = commit.HybridDateTime.DateTime, Counter = commit.HybridDateTime.Counter
- },
- ProjectId = projectId,
- Metadata = commit.Metadata,
- //without this sql cast the value will be treated as text and fail to insert into the jsonb column
- ChangeEntities = Sql.Expr>>($"{commit.ChangeEntities}::jsonb")
- })
+ .InsertWhenNotMatched()
.MergeAsync(token);
await transaction.CommitAsync(token);
diff --git a/backend/LexBoxApi/Services/EmailService.cs b/backend/LexBoxApi/Services/EmailService.cs
index ae603c65c7..faf392668a 100644
--- a/backend/LexBoxApi/Services/EmailService.cs
+++ b/backend/LexBoxApi/Services/EmailService.cs
@@ -51,7 +51,7 @@ public async Task SendNewAdminEmail(IAsyncEnumerable admins, LexAuthUser l
var email = new MimeMessage();
await foreach (var admin in admins)
{
- email.Bcc.Add(new MailboxAddress(admin.Name, admin.Email));
+ if (admin.Email is not null) email.Bcc.Add(new MailboxAddress(admin.Name, admin.Email));
}
var emailTemplate = new NewAdminEmail(
"Admin",
diff --git a/backend/LexData/DataKernel.cs b/backend/LexData/DataKernel.cs
index 79f35275c8..c1aa1774a4 100644
--- a/backend/LexData/DataKernel.cs
+++ b/backend/LexData/DataKernel.cs
@@ -1,7 +1,7 @@
using LexData.Configuration;
using LinqToDB;
-using LinqToDB.AspNet.Logging;
using LinqToDB.EntityFrameworkCore;
+using LinqToDB.Extensions.Logging;
using LinqToDB.Mapping;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
diff --git a/backend/LexData/DbError.cs b/backend/LexData/DbError.cs
index a211410d39..825ebbef56 100644
--- a/backend/LexData/DbError.cs
+++ b/backend/LexData/DbError.cs
@@ -22,8 +22,8 @@ public static DbError CreateErrorFrom(PostgresException exception)
{
return exception switch
{
- { SqlState: PostgresErrorCodes.UniqueViolation, ConstraintName: "IX_Projects_Code" } => new DbError($"{exception.TableName.Humanize().Singularize(false)} already exists", DbErrorCode.DuplicateProjectCode),
- { SqlState: PostgresErrorCodes.UniqueViolation } => new DbError($"{exception.TableName.Humanize().Singularize(false)} already exists", DbErrorCode.Duplicate),
+ { SqlState: PostgresErrorCodes.UniqueViolation, ConstraintName: "IX_Projects_Code" } => new DbError($"{exception.TableName?.Humanize().Singularize(false)} already exists", DbErrorCode.DuplicateProjectCode),
+ { SqlState: PostgresErrorCodes.UniqueViolation } => new DbError($"{exception.TableName?.Humanize().Singularize(false)} already exists", DbErrorCode.Duplicate),
_ => new DbError(exception.Message)
};
}
diff --git a/backend/LexData/LexData.csproj b/backend/LexData/LexData.csproj
index 9c5454bf25..54e2e970ea 100644
--- a/backend/LexData/LexData.csproj
+++ b/backend/LexData/LexData.csproj
@@ -2,8 +2,8 @@
-
+
all
diff --git a/backend/LexData/Migrations/20260514035126_QuartzDatabaseUpdates.Designer.cs b/backend/LexData/Migrations/20260514035126_QuartzDatabaseUpdates.Designer.cs
new file mode 100644
index 0000000000..b6723691e9
--- /dev/null
+++ b/backend/LexData/Migrations/20260514035126_QuartzDatabaseUpdates.Designer.cs
@@ -0,0 +1,1414 @@
+//
+using System;
+using System.Collections.Generic;
+using LexData;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace LexData.Migrations
+{
+ [DbContext(typeof(LexBoxDbContext))]
+ [Migration("20260514035126_QuartzDatabaseUpdates")]
+ partial class QuartzDatabaseUpdates
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False")
+ .HasAnnotation("ProductVersion", "10.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("TriggerName")
+ .HasColumnType("text")
+ .HasColumnName("trigger_name");
+
+ b.Property("TriggerGroup")
+ .HasColumnType("text")
+ .HasColumnName("trigger_group");
+
+ b.Property("BlobData")
+ .HasColumnType("bytea")
+ .HasColumnName("blob_data");
+
+ b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
+
+ b.ToTable("qrtz_blob_triggers", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("CalendarName")
+ .HasColumnType("text")
+ .HasColumnName("calendar_name");
+
+ b.Property("Calendar")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("calendar");
+
+ b.HasKey("SchedulerName", "CalendarName");
+
+ b.ToTable("qrtz_calendars", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("TriggerName")
+ .HasColumnType("text")
+ .HasColumnName("trigger_name");
+
+ b.Property("TriggerGroup")
+ .HasColumnType("text")
+ .HasColumnName("trigger_group");
+
+ b.Property("CronExpression")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("cron_expression");
+
+ b.Property("TimeZoneId")
+ .HasColumnType("text")
+ .HasColumnName("time_zone_id");
+
+ b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
+
+ b.ToTable("qrtz_cron_triggers", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("EntryId")
+ .HasColumnType("text")
+ .HasColumnName("entry_id");
+
+ b.Property("FiredTime")
+ .HasColumnType("bigint")
+ .HasColumnName("fired_time");
+
+ b.Property("InstanceName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("instance_name");
+
+ b.Property("IsNonConcurrent")
+ .HasColumnType("bool")
+ .HasColumnName("is_nonconcurrent");
+
+ b.Property("JobGroup")
+ .HasColumnType("text")
+ .HasColumnName("job_group");
+
+ b.Property("JobName")
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("RequestsRecovery")
+ .HasColumnType("bool")
+ .HasColumnName("requests_recovery");
+
+ b.Property("ScheduledTime")
+ .HasColumnType("bigint")
+ .HasColumnName("sched_time");
+
+ b.Property("State")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("state");
+
+ b.Property("TriggerGroup")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("trigger_group");
+
+ b.Property("TriggerName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("trigger_name");
+
+ b.HasKey("SchedulerName", "EntryId");
+
+ b.HasIndex("InstanceName")
+ .HasDatabaseName("idx_qrtz_ft_trig_inst_name");
+
+ b.HasIndex("JobGroup")
+ .HasDatabaseName("idx_qrtz_ft_job_group");
+
+ b.HasIndex("JobName")
+ .HasDatabaseName("idx_qrtz_ft_job_name");
+
+ b.HasIndex("RequestsRecovery")
+ .HasDatabaseName("idx_qrtz_ft_job_req_recovery");
+
+ b.HasIndex("TriggerGroup")
+ .HasDatabaseName("idx_qrtz_ft_trig_group");
+
+ b.HasIndex("TriggerName")
+ .HasDatabaseName("idx_qrtz_ft_trig_name");
+
+ b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup")
+ .HasDatabaseName("idx_qrtz_ft_trig_nm_gp");
+
+ b.ToTable("qrtz_fired_triggers", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("JobName")
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("JobGroup")
+ .HasColumnType("text")
+ .HasColumnName("job_group");
+
+ b.Property("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("IsDurable")
+ .HasColumnType("bool")
+ .HasColumnName("is_durable");
+
+ b.Property("IsNonConcurrent")
+ .HasColumnType("bool")
+ .HasColumnName("is_nonconcurrent");
+
+ b.Property("IsUpdateData")
+ .HasColumnType("bool")
+ .HasColumnName("is_update_data");
+
+ b.Property("JobClassName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_class_name");
+
+ b.Property("JobData")
+ .HasColumnType("bytea")
+ .HasColumnName("job_data");
+
+ b.Property("RequestsRecovery")
+ .HasColumnType("bool")
+ .HasColumnName("requests_recovery");
+
+ b.HasKey("SchedulerName", "JobName", "JobGroup");
+
+ b.HasIndex("RequestsRecovery")
+ .HasDatabaseName("idx_qrtz_j_req_recovery");
+
+ b.ToTable("qrtz_job_details", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("LockName")
+ .HasColumnType("text")
+ .HasColumnName("lock_name");
+
+ b.HasKey("SchedulerName", "LockName");
+
+ b.ToTable("qrtz_locks", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("TriggerGroup")
+ .HasColumnType("text")
+ .HasColumnName("trigger_group");
+
+ b.HasKey("SchedulerName", "TriggerGroup");
+
+ b.ToTable("qrtz_paused_trigger_grps", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("InstanceName")
+ .HasColumnType("text")
+ .HasColumnName("instance_name");
+
+ b.Property("CheckInInterval")
+ .HasColumnType("bigint")
+ .HasColumnName("checkin_interval");
+
+ b.Property("LastCheckInTime")
+ .HasColumnType("bigint")
+ .HasColumnName("last_checkin_time");
+
+ b.HasKey("SchedulerName", "InstanceName");
+
+ b.ToTable("qrtz_scheduler_state", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("TriggerName")
+ .HasColumnType("text")
+ .HasColumnName("trigger_name");
+
+ b.Property("TriggerGroup")
+ .HasColumnType("text")
+ .HasColumnName("trigger_group");
+
+ b.Property("BooleanProperty1")
+ .HasColumnType("bool")
+ .HasColumnName("bool_prop_1");
+
+ b.Property("BooleanProperty2")
+ .HasColumnType("bool")
+ .HasColumnName("bool_prop_2");
+
+ b.Property("DecimalProperty1")
+ .HasColumnType("numeric")
+ .HasColumnName("dec_prop_1");
+
+ b.Property("DecimalProperty2")
+ .HasColumnType("numeric")
+ .HasColumnName("dec_prop_2");
+
+ b.Property("IntegerProperty1")
+ .HasColumnType("integer")
+ .HasColumnName("int_prop_1");
+
+ b.Property("IntegerProperty2")
+ .HasColumnType("integer")
+ .HasColumnName("int_prop_2");
+
+ b.Property("LongProperty1")
+ .HasColumnType("bigint")
+ .HasColumnName("long_prop_1");
+
+ b.Property("LongProperty2")
+ .HasColumnType("bigint")
+ .HasColumnName("long_prop_2");
+
+ b.Property("StringProperty1")
+ .HasColumnType("text")
+ .HasColumnName("str_prop_1");
+
+ b.Property("StringProperty2")
+ .HasColumnType("text")
+ .HasColumnName("str_prop_2");
+
+ b.Property("StringProperty3")
+ .HasColumnType("text")
+ .HasColumnName("str_prop_3");
+
+ b.Property("TimeZoneId")
+ .HasColumnType("text")
+ .HasColumnName("time_zone_id");
+
+ b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
+
+ b.ToTable("qrtz_simprop_triggers", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("TriggerName")
+ .HasColumnType("text")
+ .HasColumnName("trigger_name");
+
+ b.Property("TriggerGroup")
+ .HasColumnType("text")
+ .HasColumnName("trigger_group");
+
+ b.Property("RepeatCount")
+ .HasColumnType("bigint")
+ .HasColumnName("repeat_count");
+
+ b.Property("RepeatInterval")
+ .HasColumnType("bigint")
+ .HasColumnName("repeat_interval");
+
+ b.Property("TimesTriggered")
+ .HasColumnType("bigint")
+ .HasColumnName("times_triggered");
+
+ b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
+
+ b.ToTable("qrtz_simple_triggers", "quartz");
+ });
+
+ modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b =>
+ {
+ b.Property("SchedulerName")
+ .HasColumnType("text")
+ .HasColumnName("sched_name");
+
+ b.Property("TriggerName")
+ .HasColumnType("text")
+ .HasColumnName("trigger_name");
+
+ b.Property("TriggerGroup")
+ .HasColumnType("text")
+ .HasColumnName("trigger_group");
+
+ b.Property("CalendarName")
+ .HasColumnType("text")
+ .HasColumnName("calendar_name");
+
+ b.Property("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property("EndTime")
+ .HasColumnType("bigint")
+ .HasColumnName("end_time");
+
+ b.Property("JobData")
+ .HasColumnType("bytea")
+ .HasColumnName("job_data");
+
+ b.Property("JobGroup")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_group");
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("MisfireInstruction")
+ .HasColumnType("smallint")
+ .HasColumnName("misfire_instr");
+
+ b.Property("MisfireOriginalFireTime")
+ .HasColumnType("bigint")
+ .HasColumnName("misfire_orig_fire_time");
+
+ b.Property("NextFireTime")
+ .HasColumnType("bigint")
+ .HasColumnName("next_fire_time");
+
+ b.Property("PreviousFireTime")
+ .HasColumnType("bigint")
+ .HasColumnName("prev_fire_time");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("StartTime")
+ .HasColumnType("bigint")
+ .HasColumnName("start_time");
+
+ b.Property("TriggerState")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("trigger_state");
+
+ b.Property("TriggerType")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("trigger_type");
+
+ b.HasKey("SchedulerName", "TriggerName", "TriggerGroup");
+
+ b.HasIndex("NextFireTime")
+ .HasDatabaseName("idx_qrtz_t_next_fire_time");
+
+ b.HasIndex("TriggerState")
+ .HasDatabaseName("idx_qrtz_t_state");
+
+ b.HasIndex("NextFireTime", "TriggerState")
+ .HasDatabaseName("idx_qrtz_t_nft_st");
+
+ b.HasIndex("SchedulerName", "JobName", "JobGroup");
+
+ b.ToTable("qrtz_triggers", "quartz");
+ });
+
+ modelBuilder.Entity("LexCore.Entities.DraftProject", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsConfidential")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("OrgId")
+ .HasColumnType("uuid");
+
+ b.Property("ProjectManagerId")
+ .HasColumnType("uuid");
+
+ b.Property("RetentionPolicy")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("ProjectManagerId");
+
+ b.ToTable("DraftProjects");
+ });
+
+ modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b =>
+ {
+ b.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("FlexModelVersion")
+ .HasColumnType("integer");
+
+ b.Property("LangProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("LexEntryCount")
+ .HasColumnType("integer");
+
+ b.HasKey("ProjectId");
+
+ b.ToTable("FlexProjectMetadata");
+ });
+
+ modelBuilder.Entity("LexCore.Entities.MediaFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Filename")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Metadata")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasDefaultValueSql("'{}'");
+
+ b.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.ToTable("Files");
+ });
+
+ modelBuilder.Entity("LexCore.Entities.OrgMember", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("OrgId")
+ .HasColumnType("uuid");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrgId");
+
+ b.HasIndex("UserId", "OrgId")
+ .IsUnique();
+
+ b.ToTable("OrgMembers", (string)null);
+ });
+
+ modelBuilder.Entity("LexCore.Entities.OrgProjects", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("OrgId")
+ .HasColumnType("uuid");
+
+ b.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("OrgId", "ProjectId")
+ .IsUnique();
+
+ b.ToTable("OrgProjects");
+ });
+
+ modelBuilder.Entity("LexCore.Entities.Organization", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .UseCollation("case_insensitive");
+
+ b.Property("UpdatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Orgs", (string)null);
+ });
+
+ modelBuilder.Entity("LexCore.Entities.Project", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("DeletedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("IsConfidential")
+ .HasColumnType("boolean");
+
+ b.Property("LastCommit")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MigratedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ParentId")
+ .HasColumnType("uuid");
+
+ b.Property("ProjectOrigin")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.Property("RepoSizeInKb")
+ .HasColumnType("integer");
+
+ b.Property("ResetStatus")
+ .HasColumnType("integer");
+
+ b.Property("RetentionPolicy")
+ .HasColumnType("integer");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("Projects");
+ });
+
+ modelBuilder.Entity("LexCore.Entities.ProjectUsers", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("UserId", "ProjectId")
+ .IsUnique();
+
+ b.ToTable("ProjectUsers");
+ });
+
+ modelBuilder.Entity("LexCore.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CanCreateProjects")
+ .HasColumnType("boolean");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Email")
+ .HasColumnType("text")
+ .UseCollation("case_insensitive");
+
+ b.Property("EmailVerified")
+ .HasColumnType("boolean");
+
+ b.Property>("FeatureFlags")
+ .HasColumnType("text[]");
+
+ b.Property("GoogleId")
+ .HasColumnType("text");
+
+ b.Property("IsAdmin")
+ .HasColumnType("boolean");
+
+ b.Property("LastActive")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LocalizationCode")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text")
+ .HasDefaultValue("en");
+
+ b.Property("Locked")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("PasswordStrength")
+ .HasColumnType("integer");
+
+ b.Property("Salt")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedDate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("now()");
+
+ b.Property("Username")
+ .HasColumnType("text")
+ .UseCollation("case_insensitive");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ApplicationType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ClientId")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ClientSecret")
+ .HasColumnType("text");
+
+ b.Property("ClientType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ConcurrencyToken")
+ .IsConcurrencyToken()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ConsentType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("DisplayNames")
+ .HasColumnType("text");
+
+ b.Property("JsonWebKeySet")
+ .HasColumnType("text");
+
+ b.Property("Permissions")
+ .HasColumnType("text");
+
+ b.Property("PostLogoutRedirectUris")
+ .HasColumnType("text");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("RedirectUris")
+ .HasColumnType("text");
+
+ b.Property("Requirements")
+ .HasColumnType("text");
+
+ b.Property("Settings")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId")
+ .IsUnique();
+
+ b.ToTable("OpenIddictApplications", (string)null);
+ });
+
+ modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ApplicationId")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyToken")
+ .IsConcurrencyToken()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("Scopes")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Subject")
+ .HasMaxLength(400)
+ .HasColumnType("character varying(400)");
+
+ b.Property("Type")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApplicationId", "Status", "Subject", "Type");
+
+ b.ToTable("OpenIddictAuthorizations", (string)null);
+ });
+
+ modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyToken")
+ .IsConcurrencyToken()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Descriptions")
+ .HasColumnType("text");
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("DisplayNames")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("Resources")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("OpenIddictScopes", (string)null);
+ });
+
+ modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ApplicationId")
+ .HasColumnType("text");
+
+ b.Property