Browse Source

Develop separate tables (#1196)

* Temp

* Simplify tests.

* Fix compose.

* Only run full test on PRs.

* Temp

* Temp

* Fix writing.
pull/1200/head
Sebastian Stehle 1 year ago
committed by GitHub
parent
commit
491edea1b4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 60
      .github/workflows/dev.yml
  2. 17
      backend/src/Squidex.Data.EntityFramework/AppDbContext.cs
  3. 27
      backend/src/Squidex.Data.EntityFramework/ContentDbContext.cs
  4. 3
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/AssetSqlQueryBuilder.cs
  5. 6
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetRepository.cs
  6. 28
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/Extensions.cs
  7. 2
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/ContentSqlQueryBuilder.cs
  8. 158
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs
  9. 30
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentBuilder.cs
  10. 18
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs
  11. 8
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentReferenceEntity.cs
  12. 22
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository.cs
  13. 70
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_Dynamic.cs
  14. 272
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs
  15. 15
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_Streaming.cs
  16. 19
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentTableEntity.cs
  17. 64
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Extensions.cs
  18. 19
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs
  19. 7
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs
  20. 23
      backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs
  21. 30
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs
  22. 16
      backend/src/Squidex.Data.EntityFramework/Infrastructure/IDbContextNamedFactory.cs
  23. 15
      backend/src/Squidex.Data.EntityFramework/Infrastructure/IDbContextWithDialect.cs
  24. 38
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/QueryExtensions.cs
  25. 4
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlQueryBuilder.cs
  26. 0
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig
  27. 4
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250204160706_Initial.Designer.cs
  28. 3
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250204160706_Initial.cs
  29. 4
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250220163956_AddFullText.Designer.cs
  30. 0
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250220163956_AddFullText.cs
  31. 1573
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250304215925_AddContentTable.Designer.cs
  32. 71
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250304215925_AddContentTable.cs
  33. 44
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs
  34. 6
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs
  35. 10
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContextDesignTimeFactory.cs
  36. 196
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/20250305193148_AddInitial.Designer.cs
  37. 116
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/20250305193148_AddInitial.cs
  38. 193
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/MySqlContentDbContextModelSnapshot.cs
  39. 38
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs
  40. 22
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContextDesignTimeFactory.cs
  41. 5
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs
  42. 2
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Extensions.cs
  43. 0
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig
  44. 4
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250204160712_Initial.Designer.cs
  45. 3
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250204160712_Initial.cs
  46. 4
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250220164002_AddFullText.Designer.cs
  47. 0
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250220164002_AddFullText.cs
  48. 1574
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250304215934_AddContentTable.Designer.cs
  49. 66
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250304215934_AddContentTable.cs
  50. 44
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs
  51. 10
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs
  52. 10
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContextDesignTimeFactory.cs
  53. 196
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/20250305193154_AddInitial.Designer.cs
  54. 83
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/20250305193154_AddInitial.cs
  55. 193
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/PostgresContentDbContextModelSnapshot.cs
  56. 32
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs
  57. 22
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContextDesignTimeFactory.cs
  58. 6
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs
  59. 0
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig
  60. 4
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250204160718_Initial.Designer.cs
  61. 3
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250204160718_Initial.cs
  62. 4
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250220164010_AddFullText.Designer.cs
  63. 0
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250220164010_AddFullText.cs
  64. 1576
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250304215941_AddContentTable.Designer.cs
  65. 65
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250304215941_AddContentTable.cs
  66. 44
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs
  67. 10
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs
  68. 10
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContextDesignTimeFactory.cs
  69. 196
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/20250305193200_AddInitial.Designer.cs
  70. 83
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/20250305193200_AddInitial.cs
  71. 193
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/SqlServerContentDbContextModelSnapshot.cs
  72. 32
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs
  73. 22
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContextDesignTimeFactory.cs
  74. 81
      backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
  75. 12
      backend/src/Squidex.Data.EntityFramework/TableName.cs
  76. 22
      backend/src/Squidex.Data.EntityFramework/run-content-migration.ps1
  77. 6
      backend/src/Squidex.Data.EntityFramework/run-migration.ps1
  78. 48
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/CollectionProvider.cs
  79. 2
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentCollection.cs
  80. 11
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository_SnapshotStore.cs
  81. 1
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryAsStream.cs
  82. 29
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryInDedicatedCollection.cs
  83. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WriteContent.cs
  84. 6
      backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs
  85. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  86. 3
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  87. 1
      backend/src/Squidex.Infrastructure/Queries/ClrValue.cs
  88. 1
      backend/src/Squidex.Web/ContextExtensions.cs
  89. 1
      backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  90. 1
      backend/src/Squidex/Config/Messaging/MessagingServices.cs
  91. 135
      backend/tests/Squidex.Data.Tests.CodeGenerator/CodeGenerator.cs
  92. 76
      backend/tests/Squidex.Data.Tests.CodeGenerator/Extensions.cs
  93. 15
      backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureModel.cs
  94. 30
      backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar
  95. 7
      backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj
  96. 23
      backend/tests/Squidex.Data.Tests.CodeGenerator/Template.handlebar
  97. 77
      backend/tests/Squidex.Data.Tests.CodeGenerator/TestGenerator.cs
  98. 8
      backend/tests/Squidex.Data.Tests.CodeGenerator/TestModel.cs
  99. 48
      backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar
  100. 6
      backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositorySnapshotTests.cs

60
.github/workflows/dev.yml

@ -41,6 +41,7 @@ jobs:
tags: squidex-local
- name: Build - TestContainers
if: github.event_name == 'pull_request'
uses: kohlerdominik/docker-run-action@v2.0.0
with:
image: mcr.microsoft.com/dotnet/sdk:8.0
@ -49,10 +50,12 @@ jobs:
run: dotnet test /src/backend/Squidex.sln --filter Category==TestContainers
- name: Test - Start Compose
if: github.event_name == 'pull_request'
run: docker compose up -d
working-directory: tools/TestSuite
- name: Test - RUN on Mongo
if: github.event_name == 'pull_request'
uses: kohlerdominik/docker-run-action@v2.0.0
with:
image: mcr.microsoft.com/dotnet/sdk:8.0
@ -66,92 +69,85 @@ jobs:
volumes: ${{ github.workspace }}:/src
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated
- name: Test - RUN on Postgres
- name: Test - RUN on Mongo on path
if: github.event_name == 'pull_request'
uses: kohlerdominik/docker-run-action@v2.0.0
with:
image: mcr.microsoft.com/dotnet/sdk:8.0
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8083
CONFIG__SERVER__URL=http://localhost:8081/squidex
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher
default_network: host
options: --name test2
options: --name test5
volumes: ${{ github.workspace }}:/src
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter "Category!=NotAutomated & Category!=MongoOnly"
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated
- name: Test - RUN on MySql
- name: Test - RUN on Postgres
if: github.event_name == 'pull_request'
uses: kohlerdominik/docker-run-action@v2.0.0
with:
image: mcr.microsoft.com/dotnet/sdk:8.0
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8084
CONFIG__SERVER__URL=http://localhost:8082
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher
default_network: host
options: --name test3
options: --name test2
volumes: ${{ github.workspace }}:/src
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter "Category!=NotAutomated & Category!=MongoOnly"
- name: Test - RUN on SqlServer
- name: Test - RUN on MySql
if: github.event_name == 'pull_request'
uses: kohlerdominik/docker-run-action@v2.0.0
with:
image: mcr.microsoft.com/dotnet/sdk:8.0
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8085
CONFIG__SERVER__URL=http://localhost:8083
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher
default_network: host
options: --name test4
options: --name test3
volumes: ${{ github.workspace }}:/src
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter "Category!=NotAutomated & Category!=MongoOnly"
- name: Test - RUN on Mongo with path
uses: kohlerdominik/docker-run-action@v2.0.0
with:
image: mcr.microsoft.com/dotnet/sdk:8.0
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8081/squidex
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher
default_network: host
options: --name test5
volumes: ${{ github.workspace }}:/src
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated
- name: Test - RUN on Mongo with dedicated collections
- name: Test - RUN on SqlServer
if: github.event_name == 'pull_request'
uses: kohlerdominik/docker-run-action@v2.0.0
with:
image: mcr.microsoft.com/dotnet/sdk:8.0
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
CONFIG__SERVER__URL=http://localhost:8082
CONFIG__SERVER__URL=http://localhost:8084
WEBHOOKCATCHER__HOST__ENDPOINT=webhookcatcher
default_network: host
options: --name test6
options: --name test4
volumes: ${{ github.workspace }}:/src
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter Category!=NotAutomated
run: dotnet test /src/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj --filter "Category!=NotAutomated & Category!=MongoOnly"
- name: Test - Install Playwright Dependencies
if: github.event_name == 'pull_request'
run: npm ci
working-directory: './tools/e2e'
- name: Test - Install Playwright Browsers
if: github.event_name == 'pull_request'
run: npx playwright install --with-deps
working-directory: './tools/e2e'
- name: Test - Run Playwright Tests
if: github.event_name == 'pull_request'
run: npx playwright test --retries=3
working-directory: './tools/e2e'
env:
BASE__URL: http://localhost:8080
- name: Test - Upload Playwright Artifacts
if: always()
if: github.event_name == 'pull_request' && always()
uses: actions/upload-artifact@v4.6.1
with:
name: playwright-report
@ -159,20 +155,20 @@ jobs:
retention-days: 30
- name: Test - Dump docker logs on failure
if: failure()
if: github.event_name == 'pull_request' && failure()
uses: jwalton/gh-docker-logs@v2.2.2
with:
dest: './docker-logs'
- name: Test - Upload docker logs
if: failure()
if: github.event_name == 'pull_request' && failure()
uses: actions/upload-artifact@v4.6.1
with:
name: docker-logs
path: './docker-logs'
- name: Test - Cleanup
if: always()
if: github.event_name == 'pull_request' && always()
run: docker compose down
working-directory: tools/TestSuite

17
backend/src/Squidex.Data.EntityFramework/AppDbContext.cs

@ -16,18 +16,22 @@ using Squidex.Domain.Apps.Entities.Jobs;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing.Consume;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.States;
using YDotNet.Server.EntityFramework;
namespace Squidex;
public class AppDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) : IdentityDbContext(options)
public abstract class AppDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) : IdentityDbContext(options), IDbContextWithDialect
{
public abstract SqlDialect Dialect { get; }
protected override void OnModelCreating(ModelBuilder builder)
{
var jsonColumnType = JsonColumnType();
var jsonColumnType = Dialect.JsonColumnType();
builder.UseApps(jsonSerializer, jsonColumnType);
builder.UseAssetKeyValueStore<TusMetadata>();
@ -35,7 +39,9 @@ public class AppDbContext(DbContextOptions options, IJsonSerializer jsonSerializ
builder.UseCache();
builder.UseCounters(jsonSerializer, jsonColumnType);
builder.UseChatStore();
builder.UseContent(jsonSerializer, jsonColumnType);
builder.UseContent(jsonSerializer, jsonColumnType, string.Empty);
builder.UseContentReferences(string.Empty);
builder.UseContentTables();
builder.UseEvents(jsonSerializer, jsonColumnType);
builder.UseEventStore();
builder.UseHistory(jsonSerializer, jsonColumnType);
@ -59,11 +65,6 @@ public class AppDbContext(DbContextOptions options, IJsonSerializer jsonSerializ
base.OnModelCreating(builder);
}
protected virtual string? JsonColumnType()
{
return null;
}
}
#pragma warning disable MA0048 // File name must match type name

27
backend/src/Squidex.Data.EntityFramework/ContentDbContext.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
namespace Squidex;
public abstract class ContentDbContext(string prefix, IJsonSerializer jsonSerializer) : DbContext, IDbContextWithDialect
{
public string Prefix { get; } = prefix;
public abstract SqlDialect Dialect { get; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseContent(jsonSerializer, Dialect.JsonColumnType(), Prefix);
base.OnModelCreating(modelBuilder);
}
}

3
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/AssetSqlQueryBuilder.cs

@ -11,7 +11,8 @@ using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Assets;
internal sealed class AssetSqlQueryBuilder(SqlDialect dialect) : SqlQueryBuilder(dialect, "Assets")
internal sealed class AssetSqlQueryBuilder(SqlDialect dialect, string tableName, SqlParams? parameters = null)
: SqlQueryBuilder(dialect, tableName, parameters)
{
public override string Visit(CompareFilter<ClrValue> nodeIn, None args)
{

6
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetRepository.cs

@ -14,8 +14,8 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets;
public sealed partial class EFAssetRepository<TContext>(IDbContextFactory<TContext> dbContextFactory, SqlDialect dialect)
: IAssetRepository where TContext : DbContext
public sealed partial class EFAssetRepository<TContext>(IDbContextFactory<TContext> dbContextFactory)
: IAssetRepository where TContext : DbContext, IDbContextWithDialect
{
public async IAsyncEnumerable<Asset> StreamAll(DomainId appId,
[EnumeratorCancellation] CancellationToken ct = default)
@ -54,7 +54,7 @@ public sealed partial class EFAssetRepository<TContext>(IDbContextFactory<TConte
}
var sqlQuery =
new AssetSqlQueryBuilder(dialect)
dbContext.AssetQuery<EFAssetEntity>()
.Where(ClrFilter.Eq(nameof(EFAssetEntity.IndexedAppId), appId));
if (q.Query.Filter?.HasField("IsDeleted") != true)

28
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/Extensions.cs

@ -0,0 +1,28 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets;
internal static class Extensions
{
public static AssetSqlQueryBuilder AssetQuery<T>(this DbContext dbContext, SqlParams? parameters = null)
{
if (dbContext is not IDbContextWithDialect withDialect)
{
throw new InvalidOperationException("Invalid context.");
}
var tableName = dbContext.Model.FindEntityType(typeof(T))?.GetTableName()
?? throw new InvalidOperationException("Unknown model.");
return new AssetSqlQueryBuilder(withDialect.Dialect, tableName, parameters);
}
}

2
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/ContentQueryBuilder.cs → backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/ContentSqlQueryBuilder.cs

@ -10,7 +10,7 @@ using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Contents;
public class ContentQueryBuilder(SqlDialect dialect, string table, SqlParams? parameters = null) : SqlQueryBuilder(dialect, table, parameters)
public class ContentSqlQueryBuilder(SqlDialect dialect, string table, SqlParams? parameters = null) : SqlQueryBuilder(dialect, table, parameters)
{
public override PropertyPath Visit(PropertyPath path)
{

158
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs

@ -0,0 +1,158 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Entities.Contents;
public readonly record struct DynamicContextName(DomainId AppId, DomainId SchemaId);
public sealed class DynamicTables<TContext, TContentContext>(
IDbContextFactory<TContext> dbContextFactory,
IDbContextNamedFactory<TContentContext> dbContextNamedFactory)
where TContext : DbContext where TContentContext : ContentDbContext
{
private readonly Dictionary<DynamicContextName, Task<string>> cachedMappings = [];
public async IAsyncEnumerable<DynamicContextName> GetContextNames(
[EnumeratorCancellation] CancellationToken ct)
{
using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);
var tableEntities = await dbContext.Set<EFContentTableEntity>().ToListAsync(ct);
lock (cachedMappings)
{
foreach (var entity in tableEntities)
{
var name = new DynamicContextName(entity.AppId, entity.SchemaId);
if (!cachedMappings.ContainsKey(name))
{
cachedMappings[name] = Task.FromResult(Prefix(entity));
}
}
}
foreach (var entity in tableEntities)
{
yield return new DynamicContextName(entity.AppId, entity.SchemaId);
}
}
public async Task<TContentContext> CreateDbContextAsync(DomainId appId, DomainId schemaId,
CancellationToken ct)
{
var prefix = await EnsureDbContextAsync(appId, schemaId);
return await dbContextNamedFactory.CreateDbContextAsync(prefix, ct);
}
public async Task<TContentContext> CreateDbContextAsync(DynamicContextName name,
CancellationToken ct)
{
var prefix = await EnsureDbContextAsync(name);
return await dbContextNamedFactory.CreateDbContextAsync(prefix, ct);
}
public async Task<string> EnsureDbContextAsync(DomainId appId, DomainId schemaId)
{
return await EnsureDbContextAsync(new DynamicContextName(appId, schemaId));
}
public async Task<string> EnsureDbContextAsync(DynamicContextName name)
{
Guard.NotDefault(name);
async Task<string> PrepareAsync()
{
var prefix = await GetPrefixAsync(name);
using var dbContext = await dbContextNamedFactory.CreateDbContextAsync(prefix, default);
// Make the prefix available as async local variable because migrations cannot use it otherwise.
TableName.Prefix = prefix;
await dbContext.Database.MigrateAsync(default);
return prefix;
}
Task<string> preparation;
lock (cachedMappings)
{
if (!cachedMappings.TryGetValue(name, out var temp))
{
temp = PrepareAsync();
cachedMappings[name] = temp;
}
preparation = temp;
}
try
{
return await preparation;
}
catch
{
// Do not cache errors forever, otherwise we would not be able to recover.
lock (cachedMappings)
{
if (cachedMappings.TryGetValue(name, out var temp) && ReferenceEquals(temp, preparation))
{
cachedMappings.Remove(name);
}
}
throw;
}
}
private async Task<string> GetPrefixAsync(DynamicContextName name)
{
var prefix = string.Empty;
await using var dbContext = await dbContextFactory.CreateDbContextAsync(default);
#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body
try
{
var entity = new EFContentTableEntity { AppId = name.AppId, SchemaId = name.SchemaId };
await dbContext.Set<EFContentTableEntity>().AddAsync(entity, default);
await dbContext.SaveChangesAsync(default);
prefix = Prefix(entity);
}
catch
{
// Very likely a unique index exception.
}
#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body
if (string.IsNullOrWhiteSpace(prefix))
{
var existing =
await dbContext.Set<EFContentTableEntity>().Where(x => x.AppId == name.AppId && x.SchemaId == name.SchemaId)
.FirstOrDefaultAsync(default)
?? throw new InvalidOperationException("Cannot resolve mapping table for schema.");
prefix = Prefix(existing);
}
return prefix;
}
private static string Prefix(EFContentTableEntity entity)
{
return $"__c{entity.Id}_";
}
}

30
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentBuilder.cs

@ -13,16 +13,31 @@ namespace Microsoft.EntityFrameworkCore;
public static class EFContentBuilder
{
public static void UseContent(this ModelBuilder builder, IJsonSerializer jsonSerializer, string? jsonColumn)
public static void UseContent(this ModelBuilder builder, IJsonSerializer jsonSerializer, string? jsonColumn, string prefix)
{
builder.UseContentEntity<EFContentCompleteEntity>("ContentsAll", jsonSerializer, jsonColumn);
builder.UseContentReference<EFReferenceCompleteEntity>("ContentReferencesAll");
builder.UseContentEntity<EFContentCompleteEntity>(jsonSerializer, jsonColumn, $"{prefix}ContentsAll");
builder.UseContentEntity<EFContentPublishedEntity>(jsonSerializer, jsonColumn, $"{prefix}ContentsPublished");
}
public static void UseContentReferences(this ModelBuilder builder, string prefix)
{
builder.UseContentReference<EFReferenceCompleteEntity>($"{prefix}ContentReferencesAll");
builder.UseContentReference<EFReferencePublishedEntity>($"{prefix}ContentReferencesPublished");
}
public static void UseContentTables(this ModelBuilder builder)
{
builder.Entity<EFContentTableEntity>(b =>
{
b.ToTable("ContentTables");
b.HasIndex("AppId", "SchemaId").IsUnique();
builder.UseContentEntity<EFContentPublishedEntity>("ContentsPublished", jsonSerializer, jsonColumn);
builder.UseContentReference<EFReferencePublishedEntity>("ContentReferencesPublished");
b.Property(x => x.AppId).AsString();
b.Property(x => x.SchemaId).AsString();
});
}
private static void UseContentEntity<T>(this ModelBuilder builder, string tableName, IJsonSerializer jsonSerializer, string? jsonColumn)
private static void UseContentEntity<T>(this ModelBuilder builder, IJsonSerializer jsonSerializer, string? jsonColumn, string tableName)
where T : EFContentEntity
{
builder.Entity<T>(b =>
@ -49,7 +64,7 @@ public static class EFContentBuilder
}
private static void UseContentReference<T>(this ModelBuilder builder, string tableName)
where T : EFReferenceEntity
where T : EFContentReferenceEntity
{
builder.Entity<T>(b =>
{
@ -58,6 +73,7 @@ public static class EFContentBuilder
b.Property(x => x.AppId).AsString();
b.Property(x => x.FromKey).AsString();
b.Property(x => x.FromSchema).AsString().HasDefaultValue(DomainId.Empty);
b.Property(x => x.ToId).AsString();
});
}

18
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs

@ -26,10 +26,17 @@ public record EFContentCompleteEntity : EFContentEntity
var source = job.Value;
var appId = source.AppId.Id;
var (referencedIds, translationStatus) = await CreateExtendedValuesAsync(source, source.CurrentVersion.Data, appProvider, ct);
var references =
referencedIds
.Select(x => new EFReferenceCompleteEntity { AppId = appId, FromKey = job.Key, ToId = x })
.Select(x => new EFReferenceCompleteEntity
{
AppId = appId,
FromKey = job.Key,
FromSchema = job.Value.SchemaId.Id,
ToId = x,
})
.ToArray();
var entity = new EFContentCompleteEntity
@ -69,10 +76,17 @@ public record EFContentPublishedEntity : EFContentEntity
var source = job.Value;
var appId = source.AppId.Id;
var (referencedIds, translationStatus) = await CreateExtendedValuesAsync(source, source.CurrentVersion.Data, appProvider, ct);
var references =
referencedIds
.Select(x => new EFReferencePublishedEntity { AppId = appId, FromKey = job.Key, ToId = x })
.Select(x => new EFReferencePublishedEntity
{
AppId = appId,
FromKey = job.Key,
FromSchema = job.Value.SchemaId.Id,
ToId = x,
})
.ToArray();
var entity = new EFContentPublishedEntity

8
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFReferenceEntity.cs → backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentReferenceEntity.cs

@ -11,19 +11,21 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents;
public sealed record EFReferenceCompleteEntity : EFReferenceEntity
public sealed record EFReferenceCompleteEntity : EFContentReferenceEntity
{
}
public sealed record EFReferencePublishedEntity : EFReferenceEntity
public sealed record EFReferencePublishedEntity : EFContentReferenceEntity
{
}
public abstract record EFReferenceEntity
public abstract record EFContentReferenceEntity
{
public DomainId AppId { get; set; }
public DomainId FromKey { get; set; }
public DomainId FromSchema { get; set; }
public DomainId ToId { get; set; }
}

22
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository.cs

@ -6,22 +6,27 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents;
public sealed partial class EFContentRepository<TContext>(
IDbContextFactory<TContext> dbContextFactory, IAppProvider appProvider,
SqlDialect dialect)
: IContentRepository where TContext : DbContext
public sealed partial class EFContentRepository<TContext, TContentContext>(
IDbContextFactory<TContext> dbContextFactory,
IDbContextNamedFactory<TContentContext> dbContentContextFactory,
IAppProvider appProvider,
IOptions<ContentsOptions> options)
: IContentRepository
where TContext : DbContext, IDbContextWithDialect where TContentContext : ContentDbContext
{
private readonly DynamicTables<TContext, TContentContext> dynamicTables = new DynamicTables<TContext, TContentContext>(dbContextFactory, dbContentContextFactory);
public async Task<Content?> FindContentAsync(App app, Schema schema, DomainId id, SearchScope scope,
CancellationToken ct = default)
{
@ -85,7 +90,7 @@ public sealed partial class EFContentRepository<TContext>(
}
public async Task<bool> HasReferrersAsync<TReference>(DomainId appId, DomainId reference,
CancellationToken ct = default) where TReference : EFReferenceEntity
CancellationToken ct = default) where TReference : EFContentReferenceEntity
{
using (Telemetry.Activities.StartActivity("EFContentRepository/QueryIdsAsync"))
{
@ -140,8 +145,9 @@ public sealed partial class EFContentRepository<TContext>(
return Task.CompletedTask;
}
private Task<TContext> CreateDbContextAsync(CancellationToken ct)
private async Task<TContext> CreateDbContextAsync(
CancellationToken ct)
{
return dbContextFactory.CreateDbContextAsync(ct);
return await dbContextFactory.CreateDbContextAsync(ct);
}
}

70
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_Dynamic.cs

@ -13,10 +13,11 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable RECS0096 // Type parameter is never used
namespace Squidex.Domain.Apps.Entities.Contents;
public sealed partial class EFContentRepository<TContext>
public sealed partial class EFContentRepository<TContext, TContentContext>
{
public Task<IResultList<Content>> QueryAsync(App app, Schema schema, Q q, SearchScope scope,
CancellationToken ct = default)
@ -43,16 +44,12 @@ public sealed partial class EFContentRepository<TContext>
schemaIds,
isSingle,
q,
"ContentsAll",
"ContentReferencesAll",
ct) :
await QueryAsync<EFContentPublishedEntity, EFReferencePublishedEntity>(
app.Id,
schemaIds,
isSingle,
q,
"ContentsPublished",
"ContentReferencesPublished",
ct);
}
}
@ -62,9 +59,7 @@ public sealed partial class EFContentRepository<TContext>
List<DomainId> schemaIds,
bool isSingle,
Q q,
string tableName,
string referenceTableName,
CancellationToken ct = default) where T : EFContentEntity where TReference : EFReferenceEntity
CancellationToken ct = default) where T : EFContentEntity
{
if (q.Ids is { Count: > 0 } && schemaIds.Count > 0)
{
@ -101,14 +96,14 @@ public sealed partial class EFContentRepository<TContext>
await using var dbContext = await CreateDbContextAsync(ct);
var queryBuilder =
new ContentQueryBuilder(dialect, tableName)
dbContext.ContentQuery<T>()
.Where(ClrFilter.In(nameof(EFContentEntity.IndexedAppId), appId))
.Where(ClrFilter.In(nameof(EFContentEntity.IndexedSchemaId), schemaIds))
.WhereQuery(nameof(EFContentEntity.Id), CompareOperator.In, (p, d) =>
new ContentQueryBuilder(d, referenceTableName, p)
.Where(ClrFilter.Eq(nameof(EFReferenceEntity.AppId), appId))
.Where(ClrFilter.Eq(nameof(EFReferenceEntity.FromKey), DomainId.Combine(appId, q.Referencing)))
.Select(nameof(EFReferenceEntity.ToId))
.WhereQuery(nameof(EFContentEntity.Id), CompareOperator.In, parameters =>
dbContext.ContentQuery<TReference>(parameters)
.Where(ClrFilter.Eq(nameof(EFContentReferenceEntity.AppId), appId))
.Where(ClrFilter.Eq(nameof(EFContentReferenceEntity.FromKey), DomainId.Combine(appId, q.Referencing)))
.Select(nameof(EFContentReferenceEntity.ToId))
)
.WhereNotDeleted(q.Query);
@ -120,12 +115,12 @@ public sealed partial class EFContentRepository<TContext>
await using var dbContext = await CreateDbContextAsync(ct);
var queryBuilder =
new ContentQueryBuilder(dialect, tableName)
.WhereQuery(nameof(EFContentEntity.DocumentId), CompareOperator.In, (p, d) =>
new ContentQueryBuilder(d, referenceTableName, p)
.Where(ClrFilter.Eq(nameof(EFReferenceEntity.AppId), appId))
.Where(ClrFilter.Eq(nameof(EFReferenceEntity.ToId), q.Reference))
.Select(nameof(EFReferenceEntity.FromKey))
dbContext.ContentQuery<T>()
.WhereQuery(nameof(EFContentEntity.DocumentId), CompareOperator.In, parameters =>
dbContext.ContentQuery<TReference>(parameters)
.Where(ClrFilter.Eq(nameof(EFContentReferenceEntity.AppId), appId))
.Where(ClrFilter.Eq(nameof(EFContentReferenceEntity.ToId), q.Reference))
.Select(nameof(EFContentReferenceEntity.FromKey))
)
.Where(ClrFilter.In(nameof(EFContentEntity.IndexedSchemaId), schemaIds))
.WhereNotDeleted(q.Query);
@ -140,21 +135,30 @@ public sealed partial class EFContentRepository<TContext>
if (isSingle)
{
await using var dbContext = await CreateDbContextAsync(ct);
var schemaId = schemaIds.Single();
var queryBuilder =
new ContentQueryBuilder(dialect, tableName)
.Where(ClrFilter.Eq(nameof(EFContentEntity.IndexedAppId), appId))
.Where(ClrFilter.Eq(nameof(EFContentEntity.IndexedSchemaId), schemaIds.Single()))
.WhereNotDeleted(q.Query);
DbContext dbContext =
dedicatedTables ?
// We are not using a transaction here, therefore we can get the table immediately.
await dynamicTables.CreateDbContextAsync(appId, schemaId, ct) :
await CreateDbContextAsync(ct);
return await QueryAsync<T>(dbContext, queryBuilder, q, ct);
await using (dbContext)
{
var queryBuilder =
dbContext.ContentQuery<T>()
.Where(ClrFilter.Eq(nameof(EFContentEntity.IndexedAppId), appId))
.Where(ClrFilter.Eq(nameof(EFContentEntity.IndexedSchemaId), schemaId))
.WhereNotDeleted(q.Query);
return await QueryAsync<T>(dbContext, queryBuilder, q, ct);
}
}
return ResultList.Empty<Content>();
}
private static async Task<IResultList<Content>> QueryAsync<T>(TContext dbContext, SqlQueryBuilder queryBuilder, Q q,
private static async Task<IResultList<Content>> QueryAsync<T>(DbContext dbContext, SqlQueryBuilder queryBuilder, Q q,
CancellationToken ct) where T : EFContentEntity
{
var result = await dbContext.QueryAsync<T>(queryBuilder, q, ct);
@ -173,19 +177,17 @@ public sealed partial class EFContentRepository<TContext>
CancellationToken ct = default)
{
return scope == SearchScope.All ?
QueryIdsAsync<EFContentCompleteEntity>(app.Id, schema.Id, filterNode,
"ContentsAll", ct) :
QueryIdsAsync<EFContentPublishedEntity>(app.Id, schema.Id, filterNode,
"ContentsPublished", ct);
QueryIdsAsync<EFContentCompleteEntity>(app.Id, schema.Id, filterNode, ct) :
QueryIdsAsync<EFContentPublishedEntity>(app.Id, schema.Id, filterNode, ct);
}
private async Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync<T>(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode, string table,
private async Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync<T>(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode,
CancellationToken ct = default) where T : EFContentEntity
{
await using var dbContext = await CreateDbContextAsync(ct);
var (sql, parameters) =
new ContentQueryBuilder(dialect, table)
dbContext.ContentQuery<T>()
.Where(ClrFilter.Eq(nameof(EFContentEntity.IndexedAppId), appId))
.Where(ClrFilter.Eq(nameof(EFContentEntity.IndexedSchemaId), schemaId))
.WhereNotDeleted(filterNode)

272
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs

@ -9,18 +9,21 @@ using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable RECS0096 // Type parameter is never used
namespace Squidex.Domain.Apps.Entities.Contents;
public sealed partial class EFContentRepository<TContext> : ISnapshotStore<WriteContent>, IDeleter
public sealed partial class EFContentRepository<TContext, TContentContext> : ISnapshotStore<WriteContent>, IDeleter
{
private readonly bool dedicatedTables = options.Value.OptimizeForSelfHosting;
async IAsyncEnumerable<SnapshotResult<WriteContent>> ISnapshotStore<WriteContent>.ReadAllAsync(
[EnumeratorCancellation] CancellationToken ct)
{
@ -34,8 +37,8 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
}
}
async Task<SnapshotResult<WriteContent>> ISnapshotStore<WriteContent>.ReadAsync(DomainId key,
CancellationToken ct)
public async Task<SnapshotResult<WriteContent>> ReadAsync(DomainId key,
CancellationToken ct = default)
{
using (Telemetry.Activities.StartActivity("EFContentRepository/ReadAsync"))
{
@ -56,29 +59,28 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
{
using (Telemetry.Activities.StartActivity("EFContentRepository/DeleteAppAsync"))
{
await using var dbContext = await CreateDbContextAsync(ct);
await using var dbTransaction = await dbContext.Database.BeginTransactionAsync(ct);
try
{
await dbContext.Set<EFContentCompleteEntity>().Where(x => x.IndexedAppId == app.Id)
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFContentPublishedEntity>().Where(x => x.IndexedAppId == app.Id)
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFReferenceCompleteEntity>().Where(x => x.AppId == app.Id)
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFReferencePublishedEntity>().Where(x => x.AppId == app.Id)
.ExecuteDeleteAsync(ct);
await DeleteCoreAsync(
x => x.IndexedAppId == app.Id,
x => x.IndexedAppId == app.Id,
x => x.AppId == app.Id,
x => x.AppId == app.Id,
x => x.AppId == app.Id,
ct);
}
}
await dbTransaction.CommitAsync(ct);
}
catch
{
await dbTransaction.RollbackAsync(ct);
throw;
}
async Task IDeleter.DeleteSchemaAsync(App app, Schema schema,
CancellationToken ct)
{
using (Telemetry.Activities.StartActivity("EFContentRepository/DeleteSchemaAsync"))
{
await DeleteCoreAsync(
x => x.IndexedAppId == app.Id && x.IndexedSchemaId == schema.Id,
x => x.IndexedAppId == app.Id && x.IndexedSchemaId == schema.Id,
x => x.AppId == app.Id && x.FromSchema == schema.Id,
x => x.AppId == app.Id && x.FromSchema == schema.Id,
x => x.AppId == app.Id && x.SchemaId == schema.Id,
ct);
}
}
@ -87,29 +89,73 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
{
using (Telemetry.Activities.StartActivity("EFContentRepository/ClearAsync"))
{
await using var dbContext = await CreateDbContextAsync(ct);
await using var dbTransaction = await dbContext.Database.BeginTransactionAsync(ct);
try
await DeleteCoreAsync(
x => true,
x => true,
x => true,
x => true,
x => true,
ct);
}
}
private async Task DeleteCoreAsync(
Expression<Func<EFContentCompleteEntity, bool>> filterComplete,
Expression<Func<EFContentPublishedEntity, bool>> filterPublished,
Expression<Func<EFReferenceCompleteEntity, bool>> filterCompleteReferences,
Expression<Func<EFReferencePublishedEntity, bool>> filterPublishedReferences,
Func<DynamicContextName, bool> filterContexts,
CancellationToken ct)
{
List<DynamicContextName>? dynamicTableNames = null;
if (dedicatedTables)
{
dynamicTableNames = await dynamicTables.GetContextNames(ct).Where(filterContexts).ToListAsync(ct);
// Ensure that the tables are created outside of the transaction.
foreach (var name in dynamicTableNames)
{
await dbContext.Set<EFContentCompleteEntity>()
.ExecuteDeleteAsync(ct);
await dynamicTables.EnsureDbContextAsync(name);
}
}
await dbContext.Set<EFContentPublishedEntity>()
.ExecuteDeleteAsync(ct);
await using var dbContext = await CreateDbContextAsync(ct);
await using var dbTransaction = await dbContext.Database.BeginTransactionAsync(ct);
try
{
await dbContext.Set<EFContentCompleteEntity>().Where(filterComplete)
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFReferenceCompleteEntity>()
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFContentPublishedEntity>().Where(filterPublished)
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFReferencePublishedEntity>()
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFReferenceCompleteEntity>().Where(filterCompleteReferences)
.ExecuteDeleteAsync(ct);
await dbTransaction.CommitAsync(ct);
}
catch
await dbContext.Set<EFReferencePublishedEntity>().Where(filterPublishedReferences)
.ExecuteDeleteAsync(ct);
// Dynamic tables are only valid if we do use dedicated tables.
if (dynamicTableNames != null)
{
await dbTransaction.RollbackAsync(ct);
throw;
foreach (var contextName in dynamicTableNames)
{
var contentDbContext = await dynamicTables.CreateDbContextAsync(contextName, ct);
await contentDbContext.Set<EFContentCompleteEntity>().Where(filterComplete)
.ExecuteDeleteAsync(ct);
await contentDbContext.Set<EFContentPublishedEntity>().Where(filterPublished)
.ExecuteDeleteAsync(ct);
}
}
await dbTransaction.CommitAsync(ct);
}
catch
{
await dbTransaction.RollbackAsync(ct);
throw;
}
}
@ -118,15 +164,35 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
{
using (Telemetry.Activities.StartActivity("EFContentRepository/RemoveAsync"))
{
WriteContent? existing = null;
if (dedicatedTables)
{
var found = await ReadAsync(key, ct);
if (found.Value == null)
{
return;
}
existing = found.Value;
// Ensure that the tables are created outside of the transaction.
await dynamicTables.EnsureDbContextAsync(existing.AppId.Id, existing.SchemaId.Id);
}
await using var dbContext = await CreateDbContextAsync(ct);
await using var dbTransaction = await dbContext.Database.BeginTransactionAsync(ct);
try
{
await dbContext.Set<EFContentCompleteEntity>().Where(x => x.DocumentId == key)
.ExecuteDeleteAsync(ct);
// The existing entity is only valid if we use dedicated tables.
if (existing != null)
{
var contentDbContext = await dynamicTables.CreateDbContextAsync(existing.AppId.Id, existing.AppId.Id, ct);
await dbContext.Set<EFContentPublishedEntity>().Where(x => x.DocumentId == key)
.ExecuteDeleteAsync(ct);
await RemoveCoreAsync(contentDbContext, key, ct);
}
// Remove this last, because we need to query the collection.
await RemoveCoreAsync(dbContext, key, ct);
await dbContext.Set<EFReferenceCompleteEntity>().Where(x => x.FromKey == key)
.ExecuteDeleteAsync(ct);
@ -144,6 +210,15 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
}
}
private static async Task RemoveCoreAsync(DbContext dbContext, DomainId key, CancellationToken ct)
{
await dbContext.Set<EFContentCompleteEntity>().Where(x => x.DocumentId == key)
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFContentPublishedEntity>().Where(x => x.DocumentId == key)
.ExecuteDeleteAsync(ct);
}
async Task ISnapshotStore<WriteContent>.WriteAsync(SnapshotWriteJob<WriteContent> job,
CancellationToken ct)
{
@ -155,30 +230,51 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
using (Telemetry.Activities.StartActivity("EFContentRepository/WriteAsync"))
{
if (dedicatedTables)
{
// Ensure that the tables are created outside of the transaction.
await dynamicTables.EnsureDbContextAsync(job.Value.AppId.Id, job.Value.SchemaId.Id);
}
await using var dbContext = await CreateDbContextAsync(ct);
await using var dbTransaction = await dbContext.Database.BeginTransactionAsync(ct);
try
{
var appId = job.Value.AppId.Id;
await dbContext.Set<EFReferenceCompleteEntity>().Where(x => x.FromKey == job.Key)
.ExecuteDeleteAsync(ct);
await dbContext.Set<EFReferencePublishedEntity>().Where(x => x.FromKey == job.Key)
.ExecuteDeleteAsync(ct);
var (completeEntity, completeReferences) = await EFContentCompleteEntity.CreateAsync(job, appProvider, ct);
await dbContext.AddRangeAsync(completeReferences);
await dbContext.UpsertAsync(completeEntity, job.OldVersion, ct);
EFContentPublishedEntity? publishedEntity = null;
if (job.Value.ShouldWritePublished())
{
await UpsertVersionedPublishedAsync(dbContext, job, ct);
var (entity, publishedReferences) = await EFContentPublishedEntity.CreateAsync(job, appProvider, ct);
await dbContext.AddRangeAsync(publishedReferences);
await dbContext.UpsertAsync(entity, job.OldVersion, ct);
publishedEntity = entity;
}
else
{
await dbContext.Set<EFContentPublishedEntity>().Where(x => x.DocumentId == job.Key)
.ExecuteDeleteAsync(ct);
await dbContext.RemoveAsync<EFContentPublishedEntity>(job.Key, ct);
}
if (dedicatedTables)
{
var contentDbContext = await dynamicTables.CreateDbContextAsync(job.Value.AppId.Id, job.Value.AppId.Id, ct);
await contentDbContext.UpsertAsync(completeEntity, job.OldVersion, ct);
await contentDbContext.UpsertOrDeleteAsync(publishedEntity, job.Key, job.OldVersion, ct);
}
await UpsertVersionedCompleteAsync(dbContext, job, ct);
await dbTransaction.CommitAsync(ct);
}
catch
@ -192,17 +288,29 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
async Task ISnapshotStore<WriteContent>.WriteManyAsync(IEnumerable<SnapshotWriteJob<WriteContent>> jobs,
CancellationToken ct)
{
// Some data is corrupt and might throw an exception if we do not ignore it.
var validJobs = jobs.Where(x => IsValid(x.Value)).ToList();
if (validJobs.Count == 0)
{
return;
}
using (Telemetry.Activities.StartActivity("EFContentRepository/WriteManyAsync"))
{
if (dedicatedTables)
{
// Ensure that the tables are created outside of the transaction.
foreach (var (appId, schemaId) in validJobs.Select(x => (AppId: x.Value.AppId.Id, SchemaId: x.Value.SchemaId.Id)).Distinct())
{
await dynamicTables.EnsureDbContextAsync(appId, schemaId);
}
}
await using var dbContext = await CreateDbContextAsync(ct);
await using var dbTransaction = await dbContext.Database.BeginTransactionAsync(ct);
try
{
var keys = validJobs.Select(x => x.Key);
var writesToCompleteContents = new List<EFContentCompleteEntity>();
var writesToCompleteReferences = new List<EFReferenceCompleteEntity>();
var writesToPublishedContents = new List<EFContentPublishedEntity>();
@ -230,8 +338,22 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
await dbContext.BulkInsertAsync(writesToCompleteReferences, cancellationToken: ct);
await dbContext.BulkInsertAsync(writesToPublishedContents, cancellationToken: ct);
await dbContext.BulkInsertAsync(writesToPublishedReferences, cancellationToken: ct);
await dbContext.SaveChangesAsync(ct);
if (dedicatedTables)
{
foreach (var bySchema in writesToCompleteContents.GroupBy(x => (AppId: x.AppId.Id, SchemaId: x.SchemaId.Id)))
{
var contentDbContext = await dynamicTables.CreateDbContextAsync(bySchema.Key.AppId, bySchema.Key.SchemaId, ct);
// Just fetch the published context, so that we can reuse the context.
var publishedContents = writesToPublishedContents.Where(x => x.AppId.Id == bySchema.Key.AppId && x.SchemaId.Id == bySchema.Key.SchemaId);
await contentDbContext.BulkInsertAsync(bySchema, cancellationToken: ct);
await contentDbContext.BulkInsertAsync(publishedContents, cancellationToken: ct);
}
}
await dbTransaction.CommitAsync(ct);
}
catch
@ -242,46 +364,6 @@ public sealed partial class EFContentRepository<TContext> : ISnapshotStore<Write
}
}
private async Task UpsertVersionedPublishedAsync(TContext dbContext, SnapshotWriteJob<WriteContent> job,
CancellationToken ct)
{
var (entity, references) = await EFContentPublishedEntity.CreateAsync(job, appProvider, ct);
await dbContext.AddRangeAsync(references);
await dbContext.UpsertAsync(entity, job.OldVersion, BuildUpdate<EFContentPublishedEntity>, ct);
}
private async Task UpsertVersionedCompleteAsync(TContext dbContext, SnapshotWriteJob<WriteContent> job,
CancellationToken ct)
{
var (entity, references) = await EFContentCompleteEntity.CreateAsync(job, appProvider, ct);
await dbContext.AddRangeAsync(references);
await dbContext.UpsertAsync(entity, job.OldVersion, BuildUpdate<EFContentCompleteEntity>, ct);
}
private static Expression<Func<SetPropertyCalls<T>, SetPropertyCalls<T>>> BuildUpdate<T>(EFContentEntity entity) where T : EFContentEntity
{
return b => b
.SetProperty(x => x.AppId, entity.AppId)
.SetProperty(x => x.Created, entity.Created)
.SetProperty(x => x.CreatedBy, entity.CreatedBy)
.SetProperty(x => x.Data, entity.Data)
.SetProperty(x => x.IndexedAppId, entity.IndexedAppId)
.SetProperty(x => x.IndexedSchemaId, entity.IndexedSchemaId)
.SetProperty(x => x.IsDeleted, entity.IsDeleted)
.SetProperty(x => x.LastModified, entity.LastModified)
.SetProperty(x => x.LastModifiedBy, entity.LastModifiedBy)
.SetProperty(x => x.NewData, entity.NewData)
.SetProperty(x => x.NewStatus, entity.NewStatus)
.SetProperty(x => x.ScheduledAt, entity.ScheduledAt)
.SetProperty(x => x.ScheduleJob, entity.ScheduleJob)
.SetProperty(x => x.SchemaId, entity.SchemaId)
.SetProperty(x => x.Status, entity.Status)
.SetProperty(x => x.TranslationStatus, entity.TranslationStatus)
.SetProperty(x => x.Version, entity.Version);
}
private static bool IsValid(WriteContent state)
{
// Some data is corrupt and might throw an exception during migration if we do not skip them.

15
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_Streaming.cs

@ -11,10 +11,11 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable RECS0096 // Type parameter is never used
namespace Squidex.Domain.Apps.Entities.Contents;
public sealed partial class EFContentRepository<TContext>
public sealed partial class EFContentRepository<TContext, TContentContext>
{
public IAsyncEnumerable<DomainId> StreamIds(DomainId appId, HashSet<DomainId>? schemaIds, SearchScope scope,
CancellationToken ct = default)
@ -78,23 +79,23 @@ public sealed partial class EFContentRepository<TContext>
}
}
public IAsyncEnumerable<Content> StreamReferencing(DomainId appId, DomainId reference, int take, SearchScope scope,
public IAsyncEnumerable<Content> StreamReferencing(DomainId appId, DomainId references, int take, SearchScope scope,
CancellationToken ct = default)
{
return scope == SearchScope.All ?
StreamReferencing<EFContentCompleteEntity, EFReferenceCompleteEntity>(appId, reference, take, ct) :
StreamReferencing<EFContentPublishedEntity, EFReferencePublishedEntity>(appId, reference, take, ct);
StreamReferencing<EFContentCompleteEntity, EFReferenceCompleteEntity>(appId, references, take, ct) :
StreamReferencing<EFContentPublishedEntity, EFReferencePublishedEntity>(appId, references, take, ct);
}
private async IAsyncEnumerable<Content> StreamReferencing<T, TReference>(DomainId appId, DomainId reference, int take,
[EnumeratorCancellation] CancellationToken ct = default) where T : EFContentEntity where TReference : EFReferenceEntity
private async IAsyncEnumerable<Content> StreamReferencing<T, TReference>(DomainId appId, DomainId references, int take,
[EnumeratorCancellation] CancellationToken ct = default) where T : EFContentEntity where TReference : EFContentReferenceEntity
{
await using var dbContext = await CreateDbContextAsync(ct);
var query =
dbContext.Set<T>()
.Join(dbContext.Set<TReference>(), t => t.DocumentId, r => r.FromKey, (t, r) => new { T = t, R = r })
.Where(x => x.R.ToId == reference)
.Where(x => x.R.ToId == references)
.Where(x => x.R.AppId == appId)
.Where(x => x.T.IndexedAppId == appId)
.Select(x => x.T).Distinct()

19
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentTableEntity.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents;
public sealed class EFContentTableEntity
{
public long Id { get; set; }
public DomainId AppId { get; set; }
public DomainId SchemaId { get; set; }
}

64
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Extensions.cs

@ -5,13 +5,77 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Contents;
internal static class Extensions
{
public static ContentSqlQueryBuilder ContentQuery<T>(this DbContext dbContext, SqlParams? parameters = null)
{
if (dbContext is not IDbContextWithDialect withDialect)
{
throw new InvalidOperationException("Invalid context.");
}
var tableName = dbContext.Model.FindEntityType(typeof(T))?.GetTableName()
?? throw new InvalidOperationException("Unknown model.");
return new ContentSqlQueryBuilder(withDialect.Dialect, tableName, parameters);
}
public static async Task UpsertAsync<T>(this DbContext dbContext, T entity, long oldVersion,
CancellationToken ct) where T : EFContentEntity
{
await dbContext.UpsertAsync(entity, oldVersion, BuildUpdate, ct);
}
public static async Task RemoveAsync<T>(this DbContext dbContext, DomainId id,
CancellationToken ct) where T : EFContentEntity
{
await dbContext.Set<T>().Where(x => x.DocumentId == id).ExecuteDeleteAsync(ct);
}
public static async Task UpsertOrDeleteAsync<T>(this DbContext dbContext, T? entity, DomainId id, long oldVersion,
CancellationToken ct) where T : EFContentEntity
{
if (entity?.Status == Status.Published && !entity.IsDeleted)
{
await UpsertAsync(dbContext, entity, oldVersion, ct);
}
else
{
await RemoveAsync<T>(dbContext, id, ct);
}
}
public static Expression<Func<SetPropertyCalls<T>, SetPropertyCalls<T>>> BuildUpdate<T>(T entity) where T : EFContentEntity
{
return b => b
.SetProperty(x => x.AppId, entity.AppId)
.SetProperty(x => x.Created, entity.Created)
.SetProperty(x => x.CreatedBy, entity.CreatedBy)
.SetProperty(x => x.Data, entity.Data)
.SetProperty(x => x.IndexedAppId, entity.IndexedAppId)
.SetProperty(x => x.IndexedSchemaId, entity.IndexedSchemaId)
.SetProperty(x => x.IsDeleted, entity.IsDeleted)
.SetProperty(x => x.LastModified, entity.LastModified)
.SetProperty(x => x.LastModifiedBy, entity.LastModifiedBy)
.SetProperty(x => x.NewData, entity.NewData)
.SetProperty(x => x.NewStatus, entity.NewStatus)
.SetProperty(x => x.ScheduledAt, entity.ScheduledAt)
.SetProperty(x => x.ScheduleJob, entity.ScheduleJob)
.SetProperty(x => x.SchemaId, entity.SchemaId)
.SetProperty(x => x.Status, entity.Status)
.SetProperty(x => x.TranslationStatus, entity.TranslationStatus)
.SetProperty(x => x.Version, entity.Version);
}
public static bool ShouldWritePublished(this WriteContent content)
{
return content.CurrentVersion.Status == Status.Published && !content.IsDeleted;

19
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs

@ -18,8 +18,9 @@ using Squidex.Providers.SqlServer;
namespace Squidex.Domain.Apps.Entities.Contents.Text;
public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextFactory, SqlDialect dialect)
: ITextIndex, IInitializable, IDeleter where TContext : DbContext
public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextFactory)
: ITextIndex, IInitializable, IDeleter
where TContext : DbContext, IDbContextWithDialect
{
private record struct SearchOperation
{
@ -39,8 +40,8 @@ public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextF
{
await using var dbContext = await CreateDbContextAsync(ct);
await dbContext.Database.CreateTextIndexAsync(dialect, "IDX_Text", "Texts", "Texts", ct);
await dbContext.Database.CreateGeoIndexAsync(dialect, "IDX_Geo", "Geos", "GeoObject", ct);
await dbContext.CreateTextIndexAsync("IDX_Text", "Texts", "Texts", ct);
await dbContext.CreateGeoIndexAsync("IDX_Geo", "Geos", "GeoObject", ct);
}
async Task IDeleter.DeleteAppAsync(App app,
@ -148,11 +149,11 @@ public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextF
return search.Results.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList();
}
private Task SearchBySchemaAsync(TContext context, SearchOperation search, IEnumerable<DomainId> schemaIds, double factor,
private static Task SearchBySchemaAsync(TContext context, SearchOperation search, IEnumerable<DomainId> schemaIds, double factor,
CancellationToken ct = default)
{
var queryBuilder =
new SqlQueryBuilder(dialect, "Texts")
context.Query<EFTextIndexTextEntity>()
.Where(ClrFilter.Eq("AppId", search.App.Id))
.Where(ClrFilter.In("SchemaId", schemaIds.ToList()))
.WhereMatch("Texts", search.SearchTerms)
@ -161,11 +162,11 @@ public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextF
return SearchAsync(context, search, queryBuilder, factor, ct);
}
private Task SearchByAppAsync(TContext context, SearchOperation search, double factor,
private static Task SearchByAppAsync(TContext context, SearchOperation search, double factor,
CancellationToken ct = default)
{
var queryBuilder =
new SqlQueryBuilder(dialect, "Texts")
context.Query<EFTextIndexTextEntity>()
.Where(ClrFilter.Eq("AppId", search.App.Id))
.WhereMatch("Texts", search.SearchTerms)
.WhereScope(search.SearchScope);
@ -242,7 +243,7 @@ public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextF
};
// We can only check the validatity by inserting them one by one.
if (dialect is SqlServerDialect)
if (dbContext.Dialect is SqlServerDialect)
{
try
{

7
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs

@ -16,8 +16,9 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Contents.Text;
public sealed class EFTextIndexerState<TContext>(IDbContextFactory<TContext> dbContextFactory, SqlDialect dialect, IContentRepository contentRepository)
: ITextIndexerState, IDeleter where TContext : DbContext
public sealed class EFTextIndexerState<TContext>(IDbContextFactory<TContext> dbContextFactory, IContentRepository contentRepository)
: ITextIndexerState, IDeleter
where TContext : DbContext, IDbContextWithDialect
{
int IDeleter.Order => -2000;
@ -27,7 +28,7 @@ public sealed class EFTextIndexerState<TContext>(IDbContextFactory<TContext> dbC
await using var dbContext = await CreateDbContextAsync(ct);
var (query, parameters) =
new SqlQueryBuilder(dialect, "TextState")
dbContext.Query<TextContentState>()
.Where(ClrFilter.Gt(nameof(TextContentState.UniqueContentId), new UniqueContentId(app.Id, DomainId.Empty).ToParseableString()))
.OrderAsc(nameof(TextContentState.UniqueContentId))
.OrderAsc(nameof(TextContentState.State))

23
backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure.Json;
namespace Squidex.Infrastructure;
public sealed class DelegatingDbNamedContextFactory<TContext>(
IJsonSerializer jsonSerializer,
Func<IJsonSerializer, string, TContext> factory)
: IDbContextNamedFactory<TContext> where TContext : DbContext
{
public Task<TContext> CreateDbContextAsync(string name,
CancellationToken ct = default)
{
return Task.FromResult(factory(jsonSerializer, name));
}
}

30
backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs

@ -7,8 +7,11 @@
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.States;
@ -16,6 +19,22 @@ namespace Squidex.Infrastructure;
public static class Extensions
{
public static IServiceCollection AddNamedDbContext<TContext>(this IServiceCollection services,
Func<IJsonSerializer, string, TContext> factory)
where TContext : DbContext
{
services.AddSingleton<IDbContextNamedFactory<TContext>>(c =>
ActivatorUtilities.CreateInstance<DelegatingDbNamedContextFactory<TContext>>(c, factory));
return services;
}
public static DbContextOptionsBuilder SetDefaultWarnings(this DbContextOptionsBuilder builder)
{
builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer));
return builder;
}
public static IQueryable<T> Pagination<T>(this IQueryable<T> source, ClrQuery query)
{
if (query.Skip > 0)
@ -110,14 +129,15 @@ public static class Extensions
Func<T, Expression<Func<SetPropertyCalls<T>, SetPropertyCalls<T>>>> update,
CancellationToken ct) where T : class, IVersionedEntity<DomainId>
{
var dbSet = dbContext.Set<T>();
try
{
await dbContext.Set<T>().AddAsync(entity, ct);
await dbSet.AddAsync(entity, ct);
await dbContext.SaveChangesAsync(ct);
}
catch (DbUpdateException)
{
var updateQuery = dbContext.Set<T>().Where(x => x.DocumentId == entity.DocumentId);
var updateQuery = dbSet.Where(x => x.DocumentId == entity.DocumentId);
if (oldVersion > EtagVersion.Any)
{
updateQuery = updateQuery.Where(x => x.Version == oldVersion);
@ -130,7 +150,7 @@ public static class Extensions
if (updateCount != 1)
{
var currentVersions =
await dbContext.Set<T>()
await dbSet
.Where(x => x.DocumentId == entity.DocumentId).Select(x => x.Version)
.ToListAsync(ct);
@ -139,5 +159,9 @@ public static class Extensions
throw new InconsistentStateException(current, oldVersion);
}
}
finally
{
dbContext.Entry(entity).State = EntityState.Detached;
}
}
}

16
backend/src/Squidex.Data.EntityFramework/Infrastructure/IDbContextNamedFactory.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
namespace Squidex.Infrastructure;
public interface IDbContextNamedFactory<TContext> where TContext : DbContext
{
Task<TContext> CreateDbContextAsync(string name,
CancellationToken ct = default);
}

15
backend/src/Squidex.Data.EntityFramework/Infrastructure/IDbContextWithDialect.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure;
public interface IDbContextWithDialect
{
SqlDialect Dialect { get; }
}

38
backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/QueryExtensions.cs

@ -6,49 +6,63 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Squidex.Infrastructure.Queries;
public static class QueryExtensions
{
public static async Task CreateGeoIndexAsync(this DatabaseFacade database, SqlDialect dialect, string name, string table, string column,
public static SqlQueryBuilder Query<T>(this DbContext dbContext, SqlParams? parameters = null)
{
if (dbContext is not IDbContextWithDialect withDialect)
{
throw new InvalidOperationException("Invalid context.");
}
var tableName = dbContext.Model.FindEntityType(typeof(T))?.GetTableName()
?? throw new InvalidOperationException("Unknown model.");
return new SqlQueryBuilder(withDialect.Dialect, tableName, parameters);
}
public static async Task CreateGeoIndexAsync<TContext>(this TContext dbContext, string name, string table, string column,
CancellationToken ct = default)
where TContext : DbContext, IDbContextWithDialect
{
var sql = dialect.GeoIndex(name, table, column);
var sql = dbContext.Dialect.GeoIndex(name, table, column);
try
{
await database.ExecuteSqlRawAsync(sql, ct);
await dbContext.Database.ExecuteSqlRawAsync(sql, ct);
}
catch (Exception ex) when (dialect.IsDuplicateIndexException(ex, name))
catch (Exception ex) when (dbContext.Dialect.IsDuplicateIndexException(ex, name))
{
// NOOP
}
}
public static async Task CreateTextIndexAsync(this DatabaseFacade database, SqlDialect dialect, string name, string table, string column,
public static async Task CreateTextIndexAsync<TContext>(this TContext dbContext, string name, string table, string column,
CancellationToken ct = default)
where TContext : DbContext, IDbContextWithDialect
{
var prepareSql = dialect.TextIndexPrepare(name);
var prepareSql = dbContext.Dialect.TextIndexPrepare(name);
if (!string.IsNullOrWhiteSpace(prepareSql))
{
try
{
await database.ExecuteSqlRawAsync(prepareSql, ct);
await dbContext.Database.ExecuteSqlRawAsync(prepareSql, ct);
}
catch (Exception ex) when (dialect.IsDuplicateIndexException(ex, name))
catch (Exception ex) when (dbContext.Dialect.IsDuplicateIndexException(ex, name))
{
// NOOP
}
}
var sql = dialect.TextIndex(name, table, column);
var sql = dbContext.Dialect.TextIndex(name, table, column);
try
{
await database.ExecuteSqlRawAsync(sql, ct);
await dbContext.Database.ExecuteSqlRawAsync(sql, ct);
}
catch (Exception ex) when (dialect.IsDuplicateIndexException(ex, name))
catch (Exception ex) when (dbContext.Dialect.IsDuplicateIndexException(ex, name))
{
// NOOP
}

4
backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlQueryBuilder.cs

@ -21,9 +21,9 @@ public class SqlQueryBuilder(SqlDialect dialect, string table, SqlParams? parame
return this;
}
public SqlQueryBuilder WhereQuery(PropertyPath path, CompareOperator op, Func<SqlParams, SqlDialect, SqlQueryBuilder> factory)
public SqlQueryBuilder WhereQuery(PropertyPath path, CompareOperator op, Func<SqlParams, SqlQueryBuilder> factory)
{
var builder = factory(sqlParameters, dialect);
var builder = factory(sqlParameters);
sqlQuery.Where.Add(dialect.WhereQuery(Visit(path), op, builder.CompileQuery(), IsJsonPath(path)));
return this;

0
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/.editorconfig → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig

4
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250204160706_Initial.Designer.cs → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250204160706_Initial.Designer.cs

@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.App;
#nullable disable
namespace Squidex.Providers.MySql.Migrations
{
[DbContext(typeof(MySqlDbContext))]
[DbContext(typeof(MySqlAppDbContext))]
[Migration("20250204160706_Initial")]
partial class Initial
{

3
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250204160706_Initial.cs → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250204160706_Initial.cs

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

4
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.Designer.cs → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250220163956_AddFullText.Designer.cs

@ -6,13 +6,13 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.App;
#nullable disable
namespace Squidex.Providers.MySql.Migrations
{
[DbContext(typeof(MySqlDbContext))]
[DbContext(typeof(MySqlAppDbContext))]
[Migration("20250220163956_AddFullText")]
partial class AddFullText
{

0
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/20250220163956_AddFullText.cs → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250220163956_AddFullText.cs

1573
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250304215925_AddContentTable.Designer.cs

File diff suppressed because it is too large

71
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250304215925_AddContentTable.cs

@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.MySql.App.Migrations
{
/// <inheritdoc />
public partial class AddContentTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "FromSchema",
table: "ContentReferencesPublished",
type: "varchar(255)",
maxLength: 255,
nullable: false,
defaultValue: "00000000-0000-0000-0000-000000000000")
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "FromSchema",
table: "ContentReferencesAll",
type: "varchar(255)",
maxLength: 255,
nullable: false,
defaultValue: "00000000-0000-0000-0000-000000000000")
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "ContentTables",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
AppId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SchemaId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_ContentTables", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_ContentTables_AppId_SchemaId",
table: "ContentTables",
columns: new[] { "AppId", "SchemaId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ContentTables");
migrationBuilder.DropColumn(
name: "FromSchema",
table: "ContentReferencesPublished");
migrationBuilder.DropColumn(
name: "FromSchema",
table: "ContentReferencesAll");
}
}
}

44
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/MySqlDbContextModelSnapshot.cs → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs

@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.App;
#nullable disable
namespace Squidex.Providers.MySql.Migrations
{
[DbContext(typeof(MySqlDbContext))]
[DbContext(typeof(MySqlAppDbContext))]
partial class MySqlDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
@ -726,6 +726,32 @@ namespace Squidex.Providers.MySql.Migrations
b.ToTable("ContentsPublished", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentTableEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("AppId", "SchemaId")
.IsUnique();
b.ToTable("ContentTables", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b =>
{
b.Property<string>("AppId")
@ -740,6 +766,13 @@ namespace Squidex.Providers.MySql.Migrations
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("FromSchema")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(255)
.HasColumnType("varchar(255)")
.HasDefaultValue("00000000-0000-0000-0000-000000000000");
b.HasKey("AppId", "FromKey", "ToId");
b.ToTable("ContentReferencesAll", (string)null);
@ -759,6 +792,13 @@ namespace Squidex.Providers.MySql.Migrations
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("FromSchema")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(255)
.HasColumnType("varchar(255)")
.HasDefaultValue("00000000-0000-0000-0000-000000000000");
b.HasKey("AppId", "FromKey", "ToId");
b.ToTable("ContentReferencesPublished", (string)null);

6
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDbContext.cs → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs

@ -7,10 +7,12 @@
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
namespace Squidex.Providers.SqlServer;
namespace Squidex.Providers.MySql.App;
public sealed class SqlServerDbContext(DbContextOptions options, IJsonSerializer jsonSerializer)
public sealed class MySqlAppDbContext(DbContextOptions options, IJsonSerializer jsonSerializer)
: AppDbContext(options, jsonSerializer)
{
public override SqlDialect Dialect => MySqlDialect.Instance;
}

10
backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDbContextDesignTimeFactory.cs → backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContextDesignTimeFactory.cs

@ -10,20 +10,20 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Squidex.Infrastructure.Json.System;
namespace Squidex.Providers.MySql;
namespace Squidex.Providers.MySql.App;
public sealed class MySqlDbContextDesignTimeFactory : IDesignTimeDbContextFactory<MySqlDbContext>
public sealed class MySqlAppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<MySqlAppDbContext>
{
public MySqlDbContext CreateDbContext(string[] args)
public MySqlAppDbContext CreateDbContext(string[] args)
{
const string ConnectionString = "Server=localhost;Port=33060;Database=test;User=mysql;Password=mysql";
var builder = new DbContextOptionsBuilder<MySqlDbContext>()
var builder = new DbContextOptionsBuilder<MySqlAppDbContext>()
.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString), options =>
{
options.UseNetTopologySuite();
});
return new MySqlDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
return new MySqlAppDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

196
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/20250305193148_AddInitial.Designer.cs

@ -0,0 +1,196 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Squidex.Providers.MySql.Content;
#nullable disable
namespace Squidex.Providers.MySql.Content.Migrations
{
[DbContext(typeof(MySqlContentDbContext))]
[Migration("20250305193148_AddInitial")]
partial class AddInitial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetime(6)");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("json");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetime(6)");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("NewData")
.HasColumnType("json");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("json");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetime(6)");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("json");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsAll", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetime(6)");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("json");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetime(6)");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("NewData")
.HasColumnType("json");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("json");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetime(6)");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("json");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsPublished", (string)null);
});
#pragma warning restore 612, 618
}
}
}

116
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/20250305193148_AddInitial.cs

@ -0,0 +1,116 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.MySql.Content.Migrations
{
/// <inheritdoc />
public partial class AddInitial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: $"{TableName.Prefix}ContentsAll",
columns: table => new
{
DocumentId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Id = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedBy = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
LastModifiedBy = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Created = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false),
AppId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
SchemaId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
NewStatus = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Status = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Data = table.Column<string>(type: "json", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ScheduleJob = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IndexedAppId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IndexedSchemaId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ScheduledAt = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true),
NewData = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
TranslationStatus = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey($"PK_{TableName.Prefix}ContentsAll", x => x.DocumentId);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: $"{TableName.Prefix}ContentsPublished",
columns: table => new
{
DocumentId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Id = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedBy = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
LastModifiedBy = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Created = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false),
AppId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
SchemaId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
NewStatus = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Status = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Data = table.Column<string>(type: "json", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ScheduleJob = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IndexedAppId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IndexedSchemaId = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ScheduledAt = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true),
NewData = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
TranslationStatus = table.Column<string>(type: "json", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey($"PK_{TableName.Prefix}ContentsPublished", x => x.DocumentId);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: $"{TableName.Prefix}ContentsAll");
migrationBuilder.DropTable(
name: $"{TableName.Prefix}ContentsPublished");
}
}
}

193
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/MySqlContentDbContextModelSnapshot.cs

@ -0,0 +1,193 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Squidex.Providers.MySql.Content;
#nullable disable
namespace Squidex.Providers.MySql.Content.Migrations
{
[DbContext(typeof(MySqlContentDbContext))]
partial class MySqlContentDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetime(6)");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("json");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetime(6)");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("NewData")
.HasColumnType("json");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("json");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetime(6)");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("json");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsAll", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetime(6)");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("json");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetime(6)");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("NewData")
.HasColumnType("json");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("json");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetime(6)");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("json");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsPublished", (string)null);
});
#pragma warning restore 612, 618
}
}
}

38
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
namespace Squidex.Providers.MySql.Content;
public sealed class MySqlContentDbContext(string prefix, string connectionString, string? versionString, IJsonSerializer jsonSerializer)
: ContentDbContext(prefix, jsonSerializer)
{
public override SqlDialect Dialect => MySqlDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var version =
!string.IsNullOrWhiteSpace(versionString) ?
ServerVersion.Parse(versionString) :
ServerVersion.AutoDetect(connectionString);
optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseMySql(connectionString, version, options =>
{
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
options.MigrationsHistoryTable($"{prefix}MigrationHistory");
});
base.OnConfiguring(optionsBuilder);
}
}

22
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContextDesignTimeFactory.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Design;
using Squidex.Infrastructure.Json.System;
namespace Squidex.Providers.MySql.Content;
public sealed class MySqlContentDbContextDesignTimeFactory : IDesignTimeDbContextFactory<MySqlContentDbContext>
{
public MySqlContentDbContext CreateDbContext(string[] args)
{
const string ConnectionString = "Server=localhost;Port=33060;Database=test;User=mysql;Password=mysql";
return new MySqlContentDbContext(string.Empty, ConnectionString, null, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

5
backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDialect.cs

@ -23,6 +23,11 @@ public sealed class MySqlDialect : SqlDialect
return exception is MySqlException ex && ex.Number == 1061;
}
public override string? JsonColumnType()
{
return "json";
}
public override string GeoIndex(string name, string table, string field)
{
return $"CREATE SPATIAL INDEX {name} ON {FormatTable(table)} ({FormatField(field, false)});";

2
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Extensions.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Extensions.cs

@ -9,7 +9,7 @@ using System.Globalization;
using System.Text;
using Squidex.Infrastructure.Queries;
namespace Squidex.Providers.Postgres;
namespace Squidex.Providers.Postgres.App;
public static class Extensions
{

0
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/.editorconfig → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig

4
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250204160712_Initial.Designer.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250204160712_Initial.Designer.cs

@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.App;
#nullable disable
namespace Squidex.Providers.Postgres.Migrations
{
[DbContext(typeof(PostgresDbContext))]
[DbContext(typeof(PostgresAppDbContext))]
[Migration("20250204160712_Initial")]
partial class Initial
{

3
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250204160712_Initial.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250204160712_Initial.cs

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable

4
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.Designer.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250220164002_AddFullText.Designer.cs

@ -6,13 +6,13 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.App;
#nullable disable
namespace Squidex.Providers.Postgres.Migrations
{
[DbContext(typeof(PostgresDbContext))]
[DbContext(typeof(PostgresAppDbContext))]
[Migration("20250220164002_AddFullText")]
partial class AddFullText
{

0
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/20250220164002_AddFullText.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250220164002_AddFullText.cs

1574
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250304215934_AddContentTable.Designer.cs

File diff suppressed because it is too large

66
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250304215934_AddContentTable.cs

@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Squidex.Providers.Postgres.App.Migrations
{
/// <inheritdoc />
public partial class AddContentTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "FromSchema",
table: "ContentReferencesPublished",
type: "character varying(255)",
maxLength: 255,
nullable: false,
defaultValue: "00000000-0000-0000-0000-000000000000");
migrationBuilder.AddColumn<string>(
name: "FromSchema",
table: "ContentReferencesAll",
type: "character varying(255)",
maxLength: 255,
nullable: false,
defaultValue: "00000000-0000-0000-0000-000000000000");
migrationBuilder.CreateTable(
name: "ContentTables",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
AppId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
SchemaId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ContentTables", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ContentTables_AppId_SchemaId",
table: "ContentTables",
columns: new[] { "AppId", "SchemaId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ContentTables");
migrationBuilder.DropColumn(
name: "FromSchema",
table: "ContentReferencesPublished");
migrationBuilder.DropColumn(
name: "FromSchema",
table: "ContentReferencesAll");
}
}
}

44
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/PostgresDbContextModelSnapshot.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs

@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.App;
#nullable disable
namespace Squidex.Providers.Postgres.Migrations
{
[DbContext(typeof(PostgresDbContext))]
[DbContext(typeof(PostgresAppDbContext))]
partial class PostgresDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
@ -727,6 +727,32 @@ namespace Squidex.Providers.Postgres.Migrations
b.ToTable("ContentsPublished", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentTableEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("AppId", "SchemaId")
.IsUnique();
b.ToTable("ContentTables", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b =>
{
b.Property<string>("AppId")
@ -741,6 +767,13 @@ namespace Squidex.Providers.Postgres.Migrations
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("FromSchema")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasDefaultValue("00000000-0000-0000-0000-000000000000");
b.HasKey("AppId", "FromKey", "ToId");
b.ToTable("ContentReferencesAll", (string)null);
@ -760,6 +793,13 @@ namespace Squidex.Providers.Postgres.Migrations
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("FromSchema")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasDefaultValue("00000000-0000-0000-0000-000000000000");
b.HasKey("AppId", "FromKey", "ToId");
b.ToTable("ContentReferencesPublished", (string)null);

10
backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDbContext.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs

@ -7,14 +7,12 @@
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
namespace Squidex.Providers.MySql;
namespace Squidex.Providers.Postgres.App;
public sealed class MySqlDbContext(DbContextOptions options, IJsonSerializer jsonSerializer)
public class PostgresAppDbContext(DbContextOptions options, IJsonSerializer jsonSerializer)
: AppDbContext(options, jsonSerializer)
{
protected override string? JsonColumnType()
{
return "json";
}
public override SqlDialect Dialect => PostgresDialect.Instance;
}

10
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDbContextDesignTimeFactory.cs → backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContextDesignTimeFactory.cs

@ -10,20 +10,20 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Squidex.Infrastructure.Json.System;
namespace Squidex.Providers.Postgres;
namespace Squidex.Providers.Postgres.App;
public sealed class PostgresDbContextDesignTimeFactory : IDesignTimeDbContextFactory<PostgresDbContext>
public sealed class PostgresAppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<PostgresAppDbContext>
{
public PostgresDbContext CreateDbContext(string[] args)
public PostgresAppDbContext CreateDbContext(string[] args)
{
const string ConnectionString = "Server=localhost;Port=54320;Database=test;User=postgres;Password=postgres";
var builder = new DbContextOptionsBuilder<PostgresDbContext>()
var builder = new DbContextOptionsBuilder<PostgresAppDbContext>()
.UseNpgsql(ConnectionString, options =>
{
options.UseNetTopologySuite();
});
return new PostgresDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
return new PostgresAppDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

196
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/20250305193154_AddInitial.Designer.cs

@ -0,0 +1,196 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Squidex.Providers.Postgres.Content;
#nullable disable
namespace Squidex.Providers.Postgres.Content.Migrations
{
[DbContext(typeof(PostgresContentDbContext))]
[Migration("20250305193154_AddInitial")]
partial class AddInitial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("NewData")
.HasColumnType("jsonb");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("jsonb");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("jsonb");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsAll", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("NewData")
.HasColumnType("jsonb");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("jsonb");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("jsonb");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsPublished", (string)null);
});
#pragma warning restore 612, 618
}
}
}

83
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/20250305193154_AddInitial.cs

@ -0,0 +1,83 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.Postgres.Content.Migrations
{
/// <inheritdoc />
public partial class AddInitial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: $"{TableName.Prefix}ContentsAll",
columns: table => new
{
DocumentId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
Id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
CreatedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
LastModifiedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Created = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false),
AppId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
SchemaId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
NewStatus = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Data = table.Column<string>(type: "jsonb", nullable: false),
ScheduleJob = table.Column<string>(type: "jsonb", nullable: true),
IndexedAppId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
IndexedSchemaId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
ScheduledAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
NewData = table.Column<string>(type: "jsonb", nullable: true),
TranslationStatus = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey($"PK_{TableName.Prefix}ContentsAll", x => x.DocumentId);
});
migrationBuilder.CreateTable(
name: $"{TableName.Prefix}ContentsPublished",
columns: table => new
{
DocumentId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
Id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
CreatedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
LastModifiedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Created = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false),
AppId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
SchemaId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
NewStatus = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Data = table.Column<string>(type: "jsonb", nullable: false),
ScheduleJob = table.Column<string>(type: "jsonb", nullable: true),
IndexedAppId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
IndexedSchemaId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
ScheduledAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
NewData = table.Column<string>(type: "jsonb", nullable: true),
TranslationStatus = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey($"PK_{TableName.Prefix}ContentsPublished", x => x.DocumentId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: $"{TableName.Prefix}ContentsAll");
migrationBuilder.DropTable(
name: $"{TableName.Prefix}ContentsPublished");
}
}
}

193
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/PostgresContentDbContextModelSnapshot.cs

@ -0,0 +1,193 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Squidex.Providers.Postgres.Content;
#nullable disable
namespace Squidex.Providers.Postgres.Content.Migrations
{
[DbContext(typeof(PostgresContentDbContext))]
partial class PostgresContentDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("NewData")
.HasColumnType("jsonb");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("jsonb");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("jsonb");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsAll", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("NewData")
.HasColumnType("jsonb");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("jsonb");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("jsonb");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsPublished", (string)null);
});
#pragma warning restore 612, 618
}
}
}

32
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
namespace Squidex.Providers.Postgres.Content;
public sealed class PostgresContentDbContext(string prefix, string connectionString, IJsonSerializer jsonSerializer)
: ContentDbContext(prefix, jsonSerializer)
{
public override SqlDialect Dialect => PostgresDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseNpgsql(connectionString, options =>
{
options.MigrationsHistoryTable($"{prefix}MigrationHistory");
});
base.OnConfiguring(optionsBuilder);
}
}

22
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContextDesignTimeFactory.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Design;
using Squidex.Infrastructure.Json.System;
namespace Squidex.Providers.Postgres.Content;
public sealed class PostgresContentDbContextDesignTimeFactory : IDesignTimeDbContextFactory<PostgresContentDbContext>
{
public PostgresContentDbContext CreateDbContext(string[] args)
{
const string ConnectionString = "Server=localhost;Port=54320;Database=test;User=postgres;Password=postgres";
return new PostgresContentDbContext(string.Empty, ConnectionString, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

6
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDialect.cs

@ -7,6 +7,7 @@
using Npgsql;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.Postgres.App;
namespace Squidex.Providers.Postgres;
@ -23,6 +24,11 @@ public class PostgresDialect : SqlDialect
return exception is PostgresException ex && ex.SqlState == "42P07";
}
public override string? JsonColumnType()
{
return "jsonb";
}
public override string GeoIndex(string name, string table, string field)
{
return $"CREATE INDEX {name} ON {FormatTable(table)} USING GIST ({FormatField(field, false)});";

0
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/.editorconfig → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig

4
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250204160718_Initial.Designer.cs → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250204160718_Initial.Designer.cs

@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.App;
#nullable disable
namespace Squidex.Providers.SqlServer.Migrations
{
[DbContext(typeof(SqlServerDbContext))]
[DbContext(typeof(SqlServerAppDbContext))]
[Migration("20250204160718_Initial")]
partial class Initial
{

3
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250204160718_Initial.cs → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250204160718_Initial.cs

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

4
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.Designer.cs → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250220164010_AddFullText.Designer.cs

@ -6,13 +6,13 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.App;
#nullable disable
namespace Squidex.Providers.SqlServer.Migrations
{
[DbContext(typeof(SqlServerDbContext))]
[DbContext(typeof(SqlServerAppDbContext))]
[Migration("20250220164010_AddFullText")]
partial class AddFullText
{

0
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/20250220164010_AddFullText.cs → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250220164010_AddFullText.cs

1576
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250304215941_AddContentTable.Designer.cs

File diff suppressed because it is too large

65
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250304215941_AddContentTable.cs

@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.SqlServer.App.Migrations
{
/// <inheritdoc />
public partial class AddContentTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "FromSchema",
table: "ContentReferencesPublished",
type: "nvarchar(255)",
maxLength: 255,
nullable: false,
defaultValue: "00000000-0000-0000-0000-000000000000");
migrationBuilder.AddColumn<string>(
name: "FromSchema",
table: "ContentReferencesAll",
type: "nvarchar(255)",
maxLength: 255,
nullable: false,
defaultValue: "00000000-0000-0000-0000-000000000000");
migrationBuilder.CreateTable(
name: "ContentTables",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
AppId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
SchemaId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ContentTables", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ContentTables_AppId_SchemaId",
table: "ContentTables",
columns: new[] { "AppId", "SchemaId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ContentTables");
migrationBuilder.DropColumn(
name: "FromSchema",
table: "ContentReferencesPublished");
migrationBuilder.DropColumn(
name: "FromSchema",
table: "ContentReferencesAll");
}
}
}

44
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs

@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.App;
#nullable disable
namespace Squidex.Providers.SqlServer.Migrations
{
[DbContext(typeof(SqlServerDbContext))]
[DbContext(typeof(SqlServerAppDbContext))]
partial class SqlServerDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
@ -729,6 +729,32 @@ namespace Squidex.Providers.SqlServer.Migrations
b.ToTable("ContentsPublished", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentTableEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.HasKey("Id");
b.HasIndex("AppId", "SchemaId")
.IsUnique();
b.ToTable("ContentTables", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFReferenceCompleteEntity", b =>
{
b.Property<string>("AppId")
@ -743,6 +769,13 @@ namespace Squidex.Providers.SqlServer.Migrations
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("FromSchema")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)")
.HasDefaultValue("00000000-0000-0000-0000-000000000000");
b.HasKey("AppId", "FromKey", "ToId");
b.ToTable("ContentReferencesAll", (string)null);
@ -762,6 +795,13 @@ namespace Squidex.Providers.SqlServer.Migrations
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("FromSchema")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)")
.HasDefaultValue("00000000-0000-0000-0000-000000000000");
b.HasKey("AppId", "FromKey", "ToId");
b.ToTable("ContentReferencesPublished", (string)null);

10
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDbContext.cs → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs

@ -7,14 +7,12 @@
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
namespace Squidex.Providers.Postgres;
namespace Squidex.Providers.SqlServer.App;
public class PostgresDbContext(DbContextOptions options, IJsonSerializer jsonSerializer)
public sealed class SqlServerAppDbContext(DbContextOptions options, IJsonSerializer jsonSerializer)
: AppDbContext(options, jsonSerializer)
{
protected override string? JsonColumnType()
{
return "jsonb";
}
public override SqlDialect Dialect => SqlServerDialect.Instance;
}

10
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDbContextDesignTimeFactory.cs → backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContextDesignTimeFactory.cs

@ -10,20 +10,20 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Squidex.Infrastructure.Json.System;
namespace Squidex.Providers.SqlServer;
namespace Squidex.Providers.SqlServer.App;
public sealed class SqlServerDbContextDesignTimeFactory : IDesignTimeDbContextFactory<SqlServerDbContext>
public sealed class SqlServerAppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<SqlServerAppDbContext>
{
public SqlServerDbContext CreateDbContext(string[] args)
public SqlServerAppDbContext CreateDbContext(string[] args)
{
const string ConnectionString = "Server=localhost;Port=14330;Database=test;User=sa;Password=sqlserver";
var builder = new DbContextOptionsBuilder<SqlServerDbContext>()
var builder = new DbContextOptionsBuilder<SqlServerAppDbContext>()
.UseSqlServer(ConnectionString, options =>
{
options.UseNetTopologySuite();
});
return new SqlServerDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
return new SqlServerAppDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

196
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/20250305193200_AddInitial.Designer.cs

@ -0,0 +1,196 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Squidex.Providers.SqlServer.Content;
#nullable disable
namespace Squidex.Providers.SqlServer.Content.Migrations
{
[DbContext(typeof(SqlServerContentDbContext))]
[Migration("20250305193200_AddInitial")]
partial class AddInitial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("NewData")
.HasColumnType("nvarchar(max)");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetimeoffset");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("nvarchar(max)");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsAll", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("NewData")
.HasColumnType("nvarchar(max)");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetimeoffset");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("nvarchar(max)");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsPublished", (string)null);
});
#pragma warning restore 612, 618
}
}
}

83
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/20250305193200_AddInitial.cs

@ -0,0 +1,83 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.SqlServer.Content.Migrations
{
/// <inheritdoc />
public partial class AddInitial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: $"{TableName.Prefix}ContentsAll",
columns: table => new
{
DocumentId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
Id = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
LastModifiedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Created = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false),
AppId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
SchemaId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
NewStatus = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Data = table.Column<string>(type: "nvarchar(max)", nullable: false),
ScheduleJob = table.Column<string>(type: "nvarchar(max)", nullable: true),
IndexedAppId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
IndexedSchemaId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
ScheduledAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
NewData = table.Column<string>(type: "nvarchar(max)", nullable: true),
TranslationStatus = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey($"PK_{TableName.Prefix}ContentsAll", x => x.DocumentId);
});
migrationBuilder.CreateTable(
name: $"{TableName.Prefix}ContentsPublished",
columns: table => new
{
DocumentId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
Id = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
LastModifiedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Created = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
Version = table.Column<long>(type: "bigint", nullable: false),
AppId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
SchemaId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
NewStatus = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Data = table.Column<string>(type: "nvarchar(max)", nullable: false),
ScheduleJob = table.Column<string>(type: "nvarchar(max)", nullable: true),
IndexedAppId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
IndexedSchemaId = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
ScheduledAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
NewData = table.Column<string>(type: "nvarchar(max)", nullable: true),
TranslationStatus = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey($"PK_{TableName.Prefix}ContentsPublished", x => x.DocumentId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: $"{TableName.Prefix}ContentsAll");
migrationBuilder.DropTable(
name: $"{TableName.Prefix}ContentsPublished");
}
}
}

193
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/SqlServerContentDbContextModelSnapshot.cs

@ -0,0 +1,193 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Squidex.Providers.SqlServer.Content;
#nullable disable
namespace Squidex.Providers.SqlServer.Content.Migrations
{
[DbContext(typeof(SqlServerContentDbContext))]
partial class SqlServerContentDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentCompleteEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("NewData")
.HasColumnType("nvarchar(max)");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetimeoffset");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("nvarchar(max)");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsAll", (string)null);
});
modelBuilder.Entity("Squidex.Domain.Apps.Entities.Contents.EFContentPublishedEntity", b =>
{
b.Property<string>("DocumentId")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("AppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Id")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedAppId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("IndexedSchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("NewData")
.HasColumnType("nvarchar(max)");
b.Property<string>("NewStatus")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ScheduleJob")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset?>("ScheduledAt")
.HasColumnType("datetimeoffset");
b.Property<string>("SchemaId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("TranslationStatus")
.HasColumnType("nvarchar(max)");
b.Property<long>("Version")
.HasColumnType("bigint");
b.HasKey("DocumentId");
b.ToTable("ContentsPublished", (string)null);
});
#pragma warning restore 612, 618
}
}
}

32
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
namespace Squidex.Providers.SqlServer.Content;
public sealed class SqlServerContentDbContext(string prefix, string connectionString, IJsonSerializer jsonSerializer)
: ContentDbContext(prefix, jsonSerializer)
{
public override SqlDialect Dialect => SqlServerDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseSqlServer(connectionString, options =>
{
options.MigrationsHistoryTable($"{prefix}MigrationHistory");
});
base.OnConfiguring(optionsBuilder);
}
}

22
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContextDesignTimeFactory.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Design;
using Squidex.Infrastructure.Json.System;
namespace Squidex.Providers.SqlServer.Content;
public sealed class SqlServerContentDbContextDesignTimeFactory : IDesignTimeDbContextFactory<SqlServerContentDbContext>
{
public SqlServerContentDbContext CreateDbContext(string[] args)
{
const string ConnectionString = "Server=localhost;Port=14330;Database=test;User=sa;Password=sqlserver";
return new SqlServerContentDbContext(string.Empty, ConnectionString, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

81
backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs

@ -7,7 +7,6 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -37,15 +36,19 @@ using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Domain.Apps.Entities.Teams;
using Squidex.Domain.Apps.Entities.Teams.Repositories;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Messaging;
using Squidex.Providers.MySql;
using Squidex.Providers.Postgres;
using Squidex.Providers.SqlServer;
using Squidex.Providers.MySql.App;
using Squidex.Providers.MySql.Content;
using Squidex.Providers.Postgres.App;
using Squidex.Providers.Postgres.Content;
using Squidex.Providers.SqlServer.App;
using Squidex.Providers.SqlServer.Content;
using YDotNet.Server.EntityFramework;
namespace Squidex;
@ -60,16 +63,16 @@ public static class ServiceExtensions
{
["MySql"] = () =>
{
services.AddDbContextFactory<MySqlDbContext>(builder =>
{
var versionString = config.GetOptionalValue<string>("store:sql:version");
var versionString = config.GetOptionalValue<string>("store:sql:version");
services.AddDbContextFactory<MySqlAppDbContext>(builder =>
{
var version =
!string.IsNullOrWhiteSpace(versionString) ?
ServerVersion.Parse(versionString) :
ServerVersion.AutoDetect(connectionString);
builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer));
builder.SetDefaultWarnings();
builder.UseMySql(connectionString, version, options =>
{
options.UseNetTopologySuite();
@ -77,44 +80,58 @@ public static class ServiceExtensions
});
});
services.AddNamedDbContext((jsonSerializer, name) =>
{
return new MySqlContentDbContext(name, connectionString, versionString, jsonSerializer);
});
services.AddSingleton(typeof(ISnapshotStore<>), typeof(MySqlSnapshotStore<>));
services.AddSingleton(MySqlDialect.Instance);
services.AddSquidexEntityFramework<MySqlDbContext>(config);
services.AddSquidexEntityFramework<MySqlAppDbContext, MySqlContentDbContext>(config);
},
["Postgres"] = () =>
{
services.AddDbContextFactory<PostgresDbContext>(builder =>
services.AddDbContextFactory<PostgresAppDbContext>(builder =>
{
builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer));
builder.SetDefaultWarnings();
builder.UseNpgsql(connectionString, options =>
{
options.UseNetTopologySuite();
});
});
services.AddNamedDbContext((jsonSerializer, name) =>
{
return new PostgresContentDbContext(name, connectionString, jsonSerializer);
});
services.AddSingleton(typeof(ISnapshotStore<>), typeof(PostgresSnapshotStore<>));
services.AddSingleton(PostgresDialect.Instance);
services.AddSquidexEntityFramework<PostgresDbContext>(config);
services.AddSquidexEntityFramework<PostgresAppDbContext, PostgresContentDbContext>(config);
},
["SqlServer"] = () =>
{
services.AddDbContextFactory<SqlServerDbContext>(builder =>
services.AddDbContextFactory<SqlServerAppDbContext>(builder =>
{
builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer));
builder.SetDefaultWarnings();
builder.UseSqlServer(connectionString, options =>
{
options.UseNetTopologySuite();
});
});
services.AddNamedDbContext((jsonSerializer, name) =>
{
return new SqlServerContentDbContext(name, connectionString, jsonSerializer);
});
services.AddSingleton(typeof(ISnapshotStore<>), typeof(SqlServerSnapshotStore<>));
services.AddSingleton(SqlServerDialect.Instance);
services.AddSquidexEntityFramework<SqlServerDbContext>(config);
services.AddSquidexEntityFramework<SqlServerAppDbContext, SqlServerContentDbContext>(config);
},
});
}
private static void AddSquidexEntityFramework<TContext>(this IServiceCollection services, IConfiguration config) where TContext : AppDbContext
private static void AddSquidexEntityFramework<TContext, TContextContext>(this IServiceCollection services, IConfiguration config)
where TContext : AppDbContext
where TContextContext : ContentDbContext
{
if (config.GetValue<bool>("store:sql:runMigration"))
{
@ -158,7 +175,7 @@ public static class ServiceExtensions
services.AddSingletonAs<EFAssetFolderRepository<TContext>>()
.As<IAssetFolderRepository>().As<ISnapshotStore<AssetFolder>>().As<IDeleter>();
services.AddSingletonAs<EFContentRepository<TContext>>()
services.AddSingletonAs<EFContentRepository<TContext, TContextContext>>()
.As<IContentRepository>().As<ISnapshotStore<WriteContent>>().As<IDeleter>();
services.AddSingletonAs<EFHistoryEventRepository<TContext>>()
@ -200,17 +217,17 @@ public static class ServiceExtensions
{
["MySql"] = () =>
{
services.AddEntityFrameworkEventStore<MySqlDbContext>(config)
services.AddEntityFrameworkEventStore<MySqlAppDbContext>(config)
.AddMysqlAdapter();
},
["Postgres"] = () =>
{
services.AddEntityFrameworkEventStore<PostgresDbContext>(config)
services.AddEntityFrameworkEventStore<PostgresAppDbContext>(config)
.AddPostgresAdapter();
},
["SqlServer"] = () =>
{
services.AddEntityFrameworkEventStore<SqlServerDbContext>(config)
services.AddEntityFrameworkEventStore<SqlServerAppDbContext>(config)
.AddSqlServerAdapter();
},
});
@ -222,33 +239,33 @@ public static class ServiceExtensions
{
["MySql"] = () =>
{
messaging.AddEntityFrameworkTransport<MySqlDbContext>(config);
messaging.AddEntityFrameworkTransport<MySqlAppDbContext>(config);
},
["Postgres"] = () =>
{
messaging.AddEntityFrameworkTransport<PostgresDbContext>(config);
messaging.AddEntityFrameworkTransport<PostgresAppDbContext>(config);
},
["SqlServer"] = () =>
{
messaging.AddEntityFrameworkTransport<SqlServerDbContext>(config);
messaging.AddEntityFrameworkTransport<SqlServerAppDbContext>(config);
},
});
return messaging;
}
public class MySqlSnapshotStore<T>(IDbContextFactory<MySqlDbContext> dbContextFactory)
: EFSnapshotStore<MySqlDbContext, T, EFState<T>>(dbContextFactory)
public class MySqlSnapshotStore<T>(IDbContextFactory<MySqlAppDbContext> dbContextFactory)
: EFSnapshotStore<MySqlAppDbContext, T, EFState<T>>(dbContextFactory)
{
}
public class PostgresSnapshotStore<T>(IDbContextFactory<PostgresDbContext> dbContextFactory)
: EFSnapshotStore<PostgresDbContext, T, EFState<T>>(dbContextFactory)
public class PostgresSnapshotStore<T>(IDbContextFactory<PostgresAppDbContext> dbContextFactory)
: EFSnapshotStore<PostgresAppDbContext, T, EFState<T>>(dbContextFactory)
{
}
public class SqlServerSnapshotStore<T>(IDbContextFactory<SqlServerDbContext> dbContextFactory)
: EFSnapshotStore<SqlServerDbContext, T, EFState<T>>(dbContextFactory)
public class SqlServerSnapshotStore<T>(IDbContextFactory<SqlServerAppDbContext> dbContextFactory)
: EFSnapshotStore<SqlServerAppDbContext, T, EFState<T>>(dbContextFactory)
{
}

12
backend/src/Squidex.Data.EntityFramework/TableName.cs

@ -0,0 +1,12 @@
namespace Squidex;
public static class TableName
{
private static readonly AsyncLocal<string> CurrentPrefix = new AsyncLocal<string>();
public static string Prefix
{
get => CurrentPrefix.Value ?? string.Empty;
set => CurrentPrefix.Value = value;
}
}

22
backend/src/Squidex.Data.EntityFramework/run-content-migration.ps1

@ -0,0 +1,22 @@
# Stop the script when a cmdlet or a native command fails
$ErrorActionPreference = 'Stop'
$migrationName = $args[0]
dotnet ef migrations add $migrationName --context MysqlContentDbContext -o Providers/MySql/Content/Migrations
if (!$?) {
Write-Error "Creating migration failed for MySql"
exit 1
}
dotnet ef migrations add $migrationName --context PostgresContentDbContext -o Providers/Postgres/Content/Migrations
if (!$?) {
Write-Error "Creating migration failed for Postgres"
exit 1
}
dotnet ef migrations add $migrationName --context SqlServerContentDbContext -o Providers/SqlServer/Content/Migrations
if (!$?) {
Write-Error "Creating migration failed for SqlServer"
exit 1
}

6
backend/src/Squidex.Data.EntityFramework/run-migration.ps1

@ -3,19 +3,19 @@ $ErrorActionPreference = 'Stop'
$migrationName = $args[0]
dotnet ef migrations add $migrationName --context MysqlDbContext -o Providers/MySql/Migrations
dotnet ef migrations add $migrationName --context MysqlAppDbContext -o Providers/MySql/App/Migrations
if (!$?) {
Write-Error "Creating migration failed for MySql"
exit 1
}
dotnet ef migrations add $migrationName --context PostgresDbContext -o Providers/Postgres/Migrations
dotnet ef migrations add $migrationName --context PostgresAppDbContext -o Providers/Postgres/App/Migrations
if (!$?) {
Write-Error "Creating migration failed for Postgres"
exit 1
}
dotnet ef migrations add $migrationName --context SqlServerDbContext -o Providers/SqlServer/Migrations
dotnet ef migrations add $migrationName --context SqlServerAppDbContext -o Providers/SqlServer/App/Migrations
if (!$?) {
Write-Error "Creating migration failed for SqlServer"
exit 1

48
backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/CollectionProvider.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Concurrent;
using MongoDB.Driver;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents;
internal class CollectionProvider(IMongoClient mongoClient, string prefixDatabase, string prefixCollection)
{
private readonly ConcurrentDictionary<(DomainId, DomainId), Task<IMongoCollection<MongoContentEntity>>> collections =
new ConcurrentDictionary<(DomainId, DomainId), Task<IMongoCollection<MongoContentEntity>>>();
public Task<IMongoCollection<MongoContentEntity>> GetCollectionAsync(DomainId appId, DomainId schemaId)
{
return collections.GetOrAdd((appId, schemaId), CreateCollectionAsync);
}
private async Task<IMongoCollection<MongoContentEntity>> CreateCollectionAsync((DomainId, DomainId) key)
{
var (appId, schemaId) = key;
var schemaDatabase = mongoClient.GetDatabase($"{prefixDatabase}_{appId}");
var schemaCollection = schemaDatabase.GetCollection<MongoContentEntity>($"{prefixCollection}_{schemaId}");
await schemaCollection.Indexes.CreateManyAsync(
[
new CreateIndexModel<MongoContentEntity>(
Builders<MongoContentEntity>.IndexKeys
.Descending(x => x.LastModified)
.Ascending(x => x.Id)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.ReferencedIds)),
new CreateIndexModel<MongoContentEntity>(
Builders<MongoContentEntity>.IndexKeys
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Descending(x => x.LastModified)),
]);
return schemaCollection;
}
}

2
backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentCollection.cs

@ -305,7 +305,7 @@ public sealed class MongoContentCollection : MongoRepositoryBase<MongoContentEnt
}
}
public async Task AddCollectionsAsync(MongoContentEntity entity, Action<IMongoCollection<MongoContentEntity>, MongoContentEntity> add,
public async Task AddToCollectionsAsync(MongoContentEntity entity, Action<IMongoCollection<MongoContentEntity>, MongoContentEntity> add,
CancellationToken ct)
{
if (queryInDedicatedCollection != null)

11
backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository_SnapshotStore.cs

@ -108,6 +108,13 @@ public partial class MongoContentRepository : ISnapshotStore<WriteContent>, IDel
async Task ISnapshotStore<WriteContent>.WriteManyAsync(IEnumerable<SnapshotWriteJob<WriteContent>> jobs,
CancellationToken ct)
{
// Some data is corrupt and might throw an exception if we do not ignore it.
var validJobs = jobs.Where(x => IsValid(x.Value)).ToList();
if (validJobs.Count == 0)
{
return;
}
using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteManyAsync"))
{
var collectionUpdates = new Dictionary<IMongoCollection<MongoContentEntity>, List<MongoContentEntity>>();
@ -121,11 +128,11 @@ public partial class MongoContentRepository : ISnapshotStore<WriteContent>, IDel
{
if (job.Value.ShouldWritePublished())
{
await collectionPublished.AddCollectionsAsync(
await collectionPublished.AddToCollectionsAsync(
await MongoContentEntity.CreatePublishedAsync(job, appProvider, ct), add, ct);
}
await collectionComplete.AddCollectionsAsync(
await collectionComplete.AddToCollectionsAsync(
await MongoContentEntity.CreateCompleteAsync(job, appProvider, ct), add, ct);
}

1
backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryAsStream.cs

@ -10,7 +10,6 @@ using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using static Google.Protobuf.WellKnownTypes.Field.Types;
namespace Squidex.Domain.Apps.Entities.Contents.Operations;

29
backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryInDedicatedCollection.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Concurrent;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
@ -18,35 +17,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Operations;
internal sealed class QueryInDedicatedCollection(IMongoClient mongoClient, string prefixDatabase, string prefixCollection) : MongoBase<MongoContentEntity>
{
private readonly ConcurrentDictionary<(DomainId, DomainId), Task<IMongoCollection<MongoContentEntity>>> collections =
new ConcurrentDictionary<(DomainId, DomainId), Task<IMongoCollection<MongoContentEntity>>>();
private readonly CollectionProvider collections = new CollectionProvider(mongoClient, prefixDatabase, prefixCollection);
public Task<IMongoCollection<MongoContentEntity>> GetCollectionAsync(DomainId appId, DomainId schemaId)
{
#pragma warning disable MA0106 // Avoid closure by using an overload with the 'factoryArgument' parameter
return collections.GetOrAdd((appId, schemaId), async key =>
{
var (appId, schemaId) = key;
var schemaDatabase = mongoClient.GetDatabase($"{prefixDatabase}_{appId}");
var schemaCollection = schemaDatabase.GetCollection<MongoContentEntity>($"{prefixCollection}_{schemaId}");
await schemaCollection.Indexes.CreateManyAsync(
[
new CreateIndexModel<MongoContentEntity>(Index
.Descending(x => x.LastModified)
.Ascending(x => x.Id)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.ReferencedIds)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Descending(x => x.LastModified)),
]);
return schemaCollection;
});
#pragma warning restore MA0106 // Avoid closure by using an overload with the 'factoryArgument' parameter
return collections.GetCollectionAsync(appId, schemaId);
}
public async Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(App app, Schema schema, FilterNode<ClrValue> filterNode,

1
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WriteContent.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Google.Protobuf.WellKnownTypes;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents;

6
backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs

@ -20,7 +20,7 @@ public static class AppProviderExtensions
var appId = schema.AppId.Id;
async Task ResolveWithIdsAsync(IField field, ReadonlyList<DomainId>? schemaIds)
async Task ResolveWithIdsAsync(ReadonlyList<DomainId>? schemaIds)
{
if (schemaIds == null)
{
@ -62,11 +62,11 @@ public static class AppProviderExtensions
switch (field)
{
case IField<ComponentFieldProperties> component:
await ResolveWithIdsAsync(field, component.Properties.SchemaIds);
await ResolveWithIdsAsync(component.Properties.SchemaIds);
break;
case IField<ComponentsFieldProperties> components:
await ResolveWithIdsAsync(field, components.Properties.SchemaIds);
await ResolveWithIdsAsync(components.Properties.SchemaIds);
break;
case IArrayField arrayField:

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -35,7 +35,7 @@ public interface IContentRepository
Task<IResultList<Content>> QueryAsync(App app, Schema schema, Q q, SearchScope scope,
CancellationToken ct = default);
Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(App app, Schema schemaId, FilterNode<ClrValue> filterNode, SearchScope scope,
Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(App app, Schema schema, FilterNode<ClrValue> filterNode, SearchScope scope,
CancellationToken ct = default);
Task<IReadOnlyList<ContentIdStatus>> QueryIdsAsync(App app, HashSet<DomainId> ids, SearchScope scope,

3
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -12,8 +12,11 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Notifications\**" />
<Compile Remove="Providers\**" />
<EmbeddedResource Remove="Notifications\**" />
<EmbeddedResource Remove="Providers\**" />
<None Remove="Notifications\**" />
<None Remove="Providers\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" />

1
backend/src/Squidex.Infrastructure/Queries/ClrValue.cs

@ -7,7 +7,6 @@
using System.Collections;
using System.Globalization;
using Google.Protobuf.WellKnownTypes;
using NodaTime;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter

1
backend/src/Squidex.Web/ContextExtensions.cs

@ -7,7 +7,6 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using RequestContext = Squidex.Domain.Apps.Entities.Context;

1
backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -17,7 +17,6 @@ using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Translations;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
using Squidex.Web;
namespace Squidex.Areas.IdentityServer.Controllers.Account;

1
backend/src/Squidex/Config/Messaging/MessagingServices.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.IO;
using System.Text.Json;
using Squidex.AI;
using Squidex.Domain.Apps.Core.Subscriptions;

135
backend/tests/Squidex.Data.Tests.CodeGenerator/CodeGenerator.cs

@ -0,0 +1,135 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text;
using HandlebarsDotNet;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Squidex.Data.Tests.CodeGenerator;
[Generator]
public class CodeGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
static TestModel? TransformTest(GeneratorSyntaxContext ctx)
{
var classSyntax = (ClassDeclarationSyntax)ctx.Node;
var className = classSyntax.Identifier.Text;
if (!className.StartsWith("EF", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (!classSyntax.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)))
{
return null;
}
if (classSyntax.TypeParameterList == null ||
classSyntax.TypeParameterList.Parameters.Count == 0 ||
classSyntax.TypeParameterList.Parameters[0].Identifier.Text != "TContext")
{
return null;
}
var reuseLabel = "default";
foreach (var attributeList in classSyntax.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var name = attribute.Name.ToString();
if (name != "ReuseLabel" && name != "ReuseLabelAttribute")
{
continue;
}
if (attribute.ArgumentList?.Arguments.Count != 1)
{
continue;
}
var value = attribute.ArgumentList.Arguments[0];
if (value.Expression is not LiteralExpressionSyntax literal)
{
continue;
}
var candidate = literal.Token.ValueText;
if (!string.IsNullOrWhiteSpace(candidate))
{
reuseLabel = candidate;
}
}
}
var namespaceDeclaration =
classSyntax.Ancestors()
.OfType<BaseNamespaceDeclarationSyntax>().First();
return new TestModel
{
BaseName = classSyntax.Identifier.Text,
ClassName = classSyntax.Identifier.Text.Substring(2),
ClassNamespace = namespaceDeclaration.Name.ToString(),
CollectionSuffix = reuseLabel.ToPascalCase(),
CollectionLabel = reuseLabel,
HasContentContext = classSyntax.TypeParameterList.Parameters.Count == 2,
};
}
var fieldDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) =>
{
return node is ClassDeclarationSyntax;
},
static (ctx, _) => TransformTest(ctx))
.Where(x => x != null);
WriteTests(context, fieldDeclarations);
WriteFixtures(context, fieldDeclarations!);
}
private static void WriteTests(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<TestModel?> fieldDeclarations)
{
var testTemplateStream = typeof(CodeGenerator).Assembly.GetManifestResourceStream("Squidex.TestTemplate.handlebar")!;
var testTemplateText = new StreamReader(testTemplateStream).ReadToEnd();
var testTemplateFunc = Handlebars.Compile(testTemplateText);
context.RegisterSourceOutput(fieldDeclarations, (context, model) =>
{
var source = testTemplateFunc(model);
context.AddSource($"{model!.BaseName}_Tests.cs", SourceText.From(source, Encoding.UTF8));
});
}
private static void WriteFixtures(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<TestModel> fieldDeclarations)
{
var fixtureTemplateStream = typeof(CodeGenerator).Assembly.GetManifestResourceStream("Squidex.FixtureTemplate.handlebar")!;
var fixtureTemplateText = new StreamReader(fixtureTemplateStream).ReadToEnd();
var fixtureTemplateFunc = Handlebars.Compile(fixtureTemplateText);
var fixtureDeclarations = fieldDeclarations.Select(
static (x, ct) =>
{
return new FixtureModel { Label = x.CollectionLabel, Name = x.CollectionSuffix };
})
.Collect().SelectMany((values, _) => values.GroupBy(x => x.Label).Select(x => x.First()));
context.RegisterSourceOutput(fixtureDeclarations, (context, model) =>
{
var source = fixtureTemplateFunc(model);
context.AddSource($"{model.Name}_Fixtures.cs", SourceText.From(source, Encoding.UTF8));
});
}
}

76
backend/tests/Squidex.Data.Tests.CodeGenerator/Extensions.cs

@ -0,0 +1,76 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text;
namespace Squidex;
public static class Extensions
{
private const char NullChar = (char)0;
public static string ToPascalCase(this string value)
{
return value.AsSpan().ToPascalCase();
}
public static string ToPascalCase(this ReadOnlySpan<char> value)
{
if (value.Length == 0)
{
return string.Empty;
}
var sb = new StringBuilder(value.Length);
var last = NullChar;
var length = 0;
for (var i = 0; i < value.Length; i++)
{
var c = value[i];
if (c == '-' || c == '_' || c == ' ')
{
if (last != NullChar)
{
sb.Append(char.ToUpperInvariant(last));
}
last = NullChar;
length = 0;
}
else
{
if (length > 1)
{
sb.Append(c);
}
else if (length == 0)
{
last = c;
}
else
{
sb.Append(char.ToUpperInvariant(last));
sb.Append(c);
last = NullChar;
}
length++;
}
}
if (last != NullChar)
{
sb.Append(char.ToUpperInvariant(last));
}
return sb.ToString();
}
}

15
backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureModel.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex;
public sealed class FixtureModel
{
public string Name { get; set; }
public string Label { get; set; }
}

30
backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar

@ -0,0 +1,30 @@
#pragma warning disable
// Auto-generated code
namespace Squidex.EntityFramework.TestHelpers;
[CollectionDefinition("Postgres{{Name}}")]
public sealed class Postgres{{Name}}FixtureCollection : ICollectionFixture<Postgres{{Name}}Fixture>
{
}
public sealed class Postgres{{Name}}Fixture() : PostgresFixture("squidex-postgres-{{Label}}")
{
}
[CollectionDefinition("MySql{{Name}}")]
public sealed class MySql{{Name}}FixtureCollection : ICollectionFixture<MySql{{Name}}Fixture>
{
}
public sealed class MySql{{Name}}Fixture() : MySqlFixture("squidex-mysql-{{Label}}")
{
}
[CollectionDefinition("SqlServer{{Name}}")]
public sealed class SqlServer{{Name}}FixtureCollection : ICollectionFixture<SqlServer{{Name}}Fixture>
{
}
public sealed class SqlServer{{Name}}Fixture() : SqlServerFixture("squidex-mssql-{{Label}}")
{
}

7
backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj

@ -10,7 +10,9 @@
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<None Remove="Template.handlebar" />
<Compile Remove="Providers\**" />
<EmbeddedResource Remove="Providers\**" />
<None Remove="Providers\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Handlebars.Net" Version="2.1.6" PrivateAssets="all" GeneratePathProperty="true" />
@ -37,6 +39,7 @@
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Template.handlebar" />
<EmbeddedResource Include="FixtureTemplate.handlebar" />
<EmbeddedResource Include="TestTemplate.handlebar" />
</ItemGroup>
</Project>

23
backend/tests/Squidex.Data.Tests.CodeGenerator/Template.handlebar

@ -1,23 +0,0 @@
#pragma warning disable
// Auto-generated code
using Squidex.EntityFramework.TestHelpers;
namespace {{classNamespace}};
[Trait("Category", "TestContainer")]
[Collection("Postgres")]
public class Postgres{{className}}(PostgresFixture fixture) : {{baseName}}<TestDbContextPostgres>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("MySql")]
public class MySql{{className}}(MySqlFixture fixture) : {{baseName}}<TestDbContextMySql>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("SqlServer")]
public class SqlServer{{className}}(SqlServerFixture fixture) : {{baseName}}<TestDbContextSqlServer>(fixture)
{
}

77
backend/tests/Squidex.Data.Tests.CodeGenerator/TestGenerator.cs

@ -1,77 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Diagnostics;
using System.Text;
using HandlebarsDotNet;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Squidex.Data.Tests.CodeGenerator;
[Generator]
public class TestGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var templateStream = typeof(TestGenerator).Assembly.GetManifestResourceStream("Squidex.Template.handlebar")!;
var templateText = new StreamReader(templateStream).ReadToEnd();
var template = Handlebars.Compile(templateText);
static TemplateModel? Transform(GeneratorSyntaxContext ctx)
{
var classSyntax = (ClassDeclarationSyntax)ctx.Node;
var className = classSyntax.Identifier.Text;
if (!className.StartsWith("EF", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (!classSyntax.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)))
{
return null;
}
if (classSyntax.TypeParameterList == null ||
classSyntax.TypeParameterList.Parameters.Count != 1 ||
classSyntax.TypeParameterList.Parameters[0].Identifier.Text != "TContext")
{
return null;
}
var namespaceDeclaration =
classSyntax.Ancestors()
.OfType<BaseNamespaceDeclarationSyntax>().First();
return new TemplateModel
{
BaseName = classSyntax.Identifier.Text,
ClassName = classSyntax.Identifier.Text.Substring(2),
ClassNamespace = namespaceDeclaration.Name.ToString(),
};
}
var fieldDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) =>
{
return node is ClassDeclarationSyntax;
},
static (ctx, _) => Transform(ctx))
.Where(x => x != null);
context.RegisterSourceOutput(fieldDeclarations, (context, model) =>
{
var source = template(model);
context.AddSource($"{model!.BaseName}_Tests.cs", SourceText.From(source, Encoding.UTF8));
});
}
}

8
backend/tests/Squidex.Data.Tests.CodeGenerator/TemplateModel.cs → backend/tests/Squidex.Data.Tests.CodeGenerator/TestModel.cs

@ -7,11 +7,17 @@
namespace Squidex;
internal sealed class TemplateModel
internal sealed class TestModel
{
public string ClassNamespace { get; set; }
public string ClassName { get; set; }
public string BaseName { get; set; }
public string CollectionSuffix { get; set; }
public string CollectionLabel { get; set; }
public bool HasContentContext { get; set; }
}

48
backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar

@ -0,0 +1,48 @@
#pragma warning disable
// Auto-generated code
using Squidex.EntityFramework.TestHelpers;
{{#if HasContentContext}}
using Squidex.Providers.MySql.Content;
using Squidex.Providers.Postgres.Content;
using Squidex.Providers.SqlServer.Content;
{{/if}}
namespace {{classNamespace}};
{{#if HasContentContext}}
[Trait("Category", "TestContainer")]
[Collection("Postgres{{CollectionSuffix}}")]
public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres, PostgresContentDbContext>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("MySql{{CollectionSuffix}}")]
public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql, MySqlContentDbContext>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("SqlServer{{CollectionSuffix}}")]
public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextSqlServer, SqlServerContentDbContext>(fixture)
{
}
{{else}}
[Trait("Category", "TestContainer")]
[Collection("Postgres{{CollectionSuffix}}")]
public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("MySql{{CollectionSuffix}}")]
public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("SqlServer{{CollectionSuffix}}")]
public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextSqlServer>(fixture)
{
}
{{/if}}

6
backend/tests/Squidex.Data.Tests/EntityFramework/Domain/Assets/EFAssetRepositorySnapshotTests.cs

@ -9,16 +9,18 @@ using Microsoft.EntityFrameworkCore;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.EntityFramework.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
using Squidex.Shared;
namespace Squidex.EntityFramework.Domain.Assets;
public abstract class EFAssetRepositorySnapshotTests<TContext>(ISqlFixture<TContext> fixture) : AssetSnapshotStoreTests where TContext : DbContext
public abstract class EFAssetRepositorySnapshotTests<TContext>(ISqlFixture<TContext> fixture) : AssetSnapshotStoreTests
where TContext : DbContext, IDbContextWithDialect
{
protected override Task<ISnapshotStore<Asset>> CreateSutAsync()
{
var sut = new EFAssetRepository<TContext>(fixture.DbContextFactory, fixture.Dialect);
var sut = new EFAssetRepository<TContext>(fixture.DbContextFactory);
return Task.FromResult<ISnapshotStore<Asset>>(sut);
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save