From 939ca6eecdf7e1514f650d0555ba7facf5c05155 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 28 May 2025 01:34:17 +0200 Subject: [PATCH] Move to Phenx (#1224) * Move to Phenx * Fix chat. * Fix tour. * Fix geography. --- .../EFAssetFolderRepository_SnapshotStore.cs | 3 +- .../Assets/EFAssetRepository_SnapshotStore.cs | 3 +- .../Apps/Entities/Contents/EFContentEntity.cs | 2 +- .../EFContentRepository_SnapshotStore.cs | 18 ++-- .../Entities/Contents/Text/EFTextIndex.cs | 10 +- .../Contents/Text/EFTextIndexerState.cs | 19 +++- .../History/EFHistoryEventRepository.cs | 3 +- .../Infrastructure/BulkInserter.cs | 29 ++++++ .../Infrastructure/Extensions.cs | 24 +++++ .../Log/EFRequestLogRepository.cs | 3 +- .../Migrations/ConnectionStringParser.cs | 2 +- .../Infrastructure/States/EFSnapshotStore.cs | 3 +- .../MySql/App/Migrations/.editorconfig | 3 + .../Migrations/20250513192106_AddCronJobs.cs | 3 +- .../Providers/MySql/App/MySqlAppDbContext.cs | 7 ++ .../MySql/Content/Migrations/.editorconfig | 3 + .../MySql/Content/MySqlContentDbContext.cs | 2 + .../Postgres/App/Migrations/.editorconfig | 3 + .../Migrations/20250513192113_AddCronJobs.cs | 3 +- .../Postgres/App/PostgresAppDbContext.cs | 7 ++ .../Postgres/Content/Migrations/.editorconfig | 3 + .../Content/PostgresContentDbContext.cs | 2 + .../App/CustomMigrations/AddFlows_Before.cs | 12 +-- .../SqlServer/App/Migrations/.editorconfig | 3 + .../Migrations/20250513192120_AddCronJobs.cs | 3 +- .../SqlServer/App/SqlServerAppDbContext.cs | 7 ++ .../Content/Migrations/.editorconfig | 3 + .../Content/SqlServerContentDbContext.cs | 2 + .../ServiceExtensions.cs | 5 + .../Squidex.Data.EntityFramework.csproj | 35 ++++--- .../Squidex.Data.MongoDb.csproj | 12 +-- .../Squidex.Domain.Apps.Core.Model.csproj | 2 +- ...Squidex.Domain.Apps.Core.Operations.csproj | 4 +- .../Squidex.Infrastructure.csproj | 14 +-- .../Translations/TranslationsController.cs | 5 + backend/src/Squidex/Squidex.csproj | 24 ++--- .../FixtureTemplate.handlebar | 9 +- .../TestTemplate.handlebar | 12 +-- .../EntityFramework/TestHelpers/BulkHelper.cs | 47 --------- .../TestHelpers/MySqlFixture.cs | 5 - .../TestHelpers/PostgresFixture.cs | 5 - .../TestHelpers/SqlServerFixture.cs | 5 - .../TestHelpers/TestDbContext.cs | 21 ++++ .../Domain/Apps/MongoAppRepositoryTests.cs | 2 +- ...MongoAssetFolderRepositorySnapshotTests.cs | 2 +- .../MongoAssetRepositorySnapshotTests.cs | 2 +- .../Assets/MongoAssetRepositoryTests.cs | 2 +- .../MongoContentRepositoryDedicatedTests.cs | 2 +- ...ContentRepositorySnapshotDedicatedTests.cs | 2 +- .../MongoContentRepositorySnapshotTests.cs | 2 +- .../Contents/MongoContentRepositoryTests.cs | 2 +- .../Contents/Text/MongoTextIndexTests.cs | 2 +- .../Text/MongoTextIndexerStateTests.cs | 2 +- .../MongoHistoryEventRepositoryTests.cs | 2 +- .../Domain/Rules/MongoRuleRepositoryTests.cs | 2 +- .../Schemas/MongoSchemaRepositoryTests.cs | 2 +- .../Domain/Schemas/MongoSchemasHashTests.cs | 2 +- .../Domain/Teams/MongoTeamRepositoryTests.cs | 2 +- .../Caching/MongoDistributedCacheTests.cs | 2 +- .../Log/MongoRequestLogRepositoryTests.cs | 2 +- .../Migrations/MongoMigrationStatusTests.cs | 2 +- .../States/MongoSnapshotStoreTests.cs | 2 +- .../MongoUsageRepositoryTests.cs | 2 +- .../MongoDb/TestHelpers/MongoFixture.cs | 3 +- .../Contents/Text/TextIndexerTests.cs | 5 +- .../pages/onboarding-dialog.component.html | 17 +--- .../contributors-page.component.html | 2 +- .../angular/modals/tour-step.directive.ts | 13 ++- .../modals/tour-template.component.html | 2 +- .../framework/angular/modals/tour.service.ts | 96 +++++++++++++++---- .../components/chat-dialog.component.ts | 43 ++++++--- .../components/chat-item.component.html | 8 +- .../shared/components/chat-item.component.ts | 2 +- .../components/tour-guide.component.html | 4 - .../components/tour-guide.component.scss | 1 + .../shared/components/tour-guide.component.ts | 4 - .../src/app/shared/services/rules.service.ts | 2 +- .../shared/services/translations.service.ts | 12 ++- frontend/src/app/shared/state/tour.state.ts | 3 +- frontend/src/app/shared/state/tour.tasks.ts | 7 +- .../pages/internal/chat-menu.component.html | 2 +- .../pages/internal/chat-menu.component.ts | 3 +- frontend/src/app/theme/_bootstrap.scss | 4 + frontend/src/app/theme/_common.scss | 4 + 84 files changed, 412 insertions(+), 255 deletions(-) create mode 100644 backend/src/Squidex.Data.EntityFramework/Infrastructure/BulkInserter.cs delete mode 100644 backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/BulkHelper.cs diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetFolderRepository_SnapshotStore.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetFolderRepository_SnapshotStore.cs index abe38eeb6..e4497d621 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetFolderRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetFolderRepository_SnapshotStore.cs @@ -7,7 +7,6 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; -using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Squidex.Domain.Apps.Core.Apps; @@ -105,7 +104,7 @@ public sealed partial class EFAssetFolderRepository : ISnapshotStore : ISnapshotStore, } await using var dbContext = await CreateDbContextAsync(ct); - await dbContext.BulkInsertAsync(entities, cancellationToken: ct); + await dbContext.BulkInsertAsync(entities, ct); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs index 778bf6769..8d35fbe07 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs @@ -188,7 +188,7 @@ public record EFContentEntity : Content, IVersionedEntity if (data.CanHaveReference()) { - var components = await appProvider.GetComponentsAsync(schema, ct: ct); + var components = await appProvider.GetComponentsAsync(schema, ct); data.AddReferencedIds(schema, referencedIds, components); } diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs index 38310f718..9de939372 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs @@ -7,7 +7,6 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; -using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; @@ -334,10 +333,10 @@ public sealed partial class EFContentRepository : ISn } } - await dbContext.BulkInsertAsync(writesToCompleteContents, cancellationToken: ct); - await dbContext.BulkInsertAsync(writesToCompleteReferences, cancellationToken: ct); - await dbContext.BulkInsertAsync(writesToPublishedContents, cancellationToken: ct); - await dbContext.BulkInsertAsync(writesToPublishedReferences, cancellationToken: ct); + await dbContext.BulkInsertAsync(writesToCompleteContents, ct); + await dbContext.BulkInsertAsync(writesToCompleteReferences, ct); + await dbContext.BulkInsertAsync(writesToPublishedContents, ct); + await dbContext.BulkInsertAsync(writesToPublishedReferences, ct); await dbContext.SaveChangesAsync(ct); if (dedicatedTables) @@ -347,10 +346,13 @@ public sealed partial class EFContentRepository : ISn 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); + var publishedContents = + writesToPublishedContents + .Where(x => x.AppId.Id == bySchema.Key.AppId && x.SchemaId.Id == bySchema.Key.SchemaId) + .ToList(); - await contentDbContext.BulkInsertAsync(bySchema, cancellationToken: ct); - await contentDbContext.BulkInsertAsync(publishedContents, cancellationToken: ct); + await contentDbContext.BulkInsertAsync(bySchema.ToList(), ct); + await contentDbContext.BulkInsertAsync(publishedContents, ct); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs index e1cecfae3..d82bbc267 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using EFCore.BulkExtensions; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using NetTopologySuite.Geometries; @@ -87,7 +86,10 @@ public sealed class EFTextIndex(IDbContextFactory dbContextF await using var dbContext = await CreateDbContextAsync(ct); - var point = new Point(query.Longitude, query.Latitude) { SRID = 4326 }; + var point = new Point(query.Longitude, query.Latitude) + { + SRID = 4326, + }; // The distance must be converted to decrees (in contrast to MongoDB, which uses radian). var degrees = query.Radius / 111320; @@ -291,8 +293,8 @@ public sealed class EFTextIndex(IDbContextFactory dbContextF } } - await dbContext.BulkInsertOrUpdateAsync(insertsText, cancellationToken: ct); - await dbContext.BulkInsertOrUpdateAsync(insertsGeo, cancellationToken: ct); + await dbContext.BulkUpsertAsync(insertsText, ct); + await dbContext.BulkUpsertAsync(insertsGeo, ct); } private Task CreateDbContextAsync(CancellationToken ct) diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs index abc2a3a78..1c8b2c6cf 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Schemas; @@ -90,14 +89,14 @@ public sealed class EFTextIndexerState(IDbContextFactory dbC public async Task SetAsync(List updates, CancellationToken ct = default) { - var toDelete = new List(); + var toDelete = new List(); var toUpsert = new List(); foreach (var update in updates) { if (update.State == TextState.Deleted) { - toDelete.Add(update); + toDelete.Add(update.UniqueContentId); } else { @@ -111,8 +110,18 @@ public sealed class EFTextIndexerState(IDbContextFactory dbC } await using var dbContext = await CreateDbContextAsync(ct); - await dbContext.BulkDeleteAsync(toDelete, cancellationToken: ct); - await dbContext.BulkInsertOrUpdateAsync(toUpsert, cancellationToken: ct); + + if (toUpsert.Count > 0) + { + await dbContext.BulkUpsertAsync(toUpsert, ct); + } + + if (toDelete.Count > 0) + { + await dbContext.Set() + .Where(x => toDelete.Contains(x.UniqueContentId)) + .ExecuteDeleteAsync(ct); + } } private Task CreateDbContextAsync(CancellationToken ct) diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/History/EFHistoryEventRepository.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/History/EFHistoryEventRepository.cs index 773eaa1d3..20257e5f6 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/History/EFHistoryEventRepository.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/History/EFHistoryEventRepository.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.History.Repositories; @@ -65,7 +64,7 @@ public sealed class EFHistoryEventRepository(IDbContextFactory CreateDbContextAsync(CancellationToken ct) diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/BulkInserter.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/BulkInserter.cs new file mode 100644 index 000000000..c8955a71e --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/BulkInserter.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.Extensions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; +using Squidex.Events.EntityFramework; +using Squidex.Flows.EntityFramework; + +namespace Squidex.Infrastructure; + +public sealed class BulkInserter : IDbFlowsBulkInserter, IDbEventStoreBulkInserter +{ + public Task BulkInsertAsync(DbContext dbContext, IEnumerable entities, + CancellationToken ct = default) where T : class + { + return dbContext.ExecuteBulkInsertAsync(entities, cancellationToken: ct); + } + + public Task BulkUpsertAsync(DbContext dbContext, IEnumerable entities, + CancellationToken ct = default) where T : class + { + return dbContext.ExecuteBulkInsertAsync(entities, o => { }, new OnConflictOptions { Update = e => e }, ct); + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs index 8112a4d94..c9141f5aa 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs @@ -10,6 +10,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.DependencyInjection; +using PhenX.EntityFrameworkCore.BulkInsert.Extensions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -60,6 +62,28 @@ public static class Extensions return source.Where(predicate); } + public static Task BulkUpsertAsync(this DbContext dbContext, List source, + CancellationToken ct) where T : class + { + if (source.Count == 0) + { + return Task.CompletedTask; + } + + return dbContext.ExecuteBulkInsertAsync(source, o => { }, new OnConflictOptions { Update = e => e }, ct); + } + + public static Task BulkInsertAsync(this DbContext dbContext, List source, + CancellationToken ct) where T : class + { + if (source.Count == 0) + { + return Task.CompletedTask; + } + + return dbContext.ExecuteBulkInsertAsync(source, cancellationToken: ct); + } + public static async Task> QueryAsync(this IQueryable queryable, Q q, CancellationToken ct) where T : class { diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Log/EFRequestLogRepository.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Log/EFRequestLogRepository.cs index 2271bb03e..aa65d2083 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Log/EFRequestLogRepository.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Log/EFRequestLogRepository.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Runtime.CompilerServices; -using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using NodaTime; @@ -72,7 +71,7 @@ public sealed class EFRequestLogRepository(IDbContextFactory await using var dbContext = await CreateDbContextAsync(ct); - await dbContext.BulkInsertAsync(entities, cancellationToken: ct); + await dbContext.BulkInsertAsync(entities, ct); } public async IAsyncEnumerable QueryAllAsync(string key, Instant fromTime, Instant toTime, diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs index 3f19a3a2c..3d7c65916 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs @@ -28,7 +28,7 @@ public class ConnectionStringParser { var builder = new DbConnectionStringBuilder { - ConnectionString = source + ConnectionString = source, }; if (builder.TryGetValue("Server", out var server)) diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/States/EFSnapshotStore.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/States/EFSnapshotStore.cs index 02fb37ad6..3e74d3867 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/States/EFSnapshotStore.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/States/EFSnapshotStore.cs @@ -7,7 +7,6 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; -using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; @@ -106,7 +105,7 @@ public class EFSnapshotStore(IDbContextFactory db } await using var dbContext = await CreateDbContextAsync(ct); - await dbContext.BulkInsertOrUpdateAsync(entities, cancellationToken: ct); + await dbContext.BulkUpsertAsync(entities, ct); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig index 1e79455e5..fb1b88333 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig @@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none # MA0048: File name must match type name dotnet_diagnostic.MA0048.severity = none +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none + # SA1633: File must have header dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs index 254ef4351..d4ac528ed 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs index 370381d5a..14d40466b 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.MySql; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -15,4 +16,10 @@ public sealed class MySqlAppDbContext(DbContextOptions options, IJsonSerializer : AppDbContext(options, jsonSerializer) { public override SqlDialect Dialect => MySqlDialect.Instance; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseBulkInsertMySql(); + base.OnConfiguring(optionsBuilder); + } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/.editorconfig index 1e79455e5..fb1b88333 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/.editorconfig +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/.editorconfig @@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none # MA0048: File name must match type name dotnet_diagnostic.MA0048.severity = none +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none + # SA1633: File must have header dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs index cb56c37d2..8071b88a9 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.MySql; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -27,6 +28,7 @@ public sealed class MySqlContentDbContext(string prefix, string connectionString ServerVersion.AutoDetect(connectionString); optionsBuilder.SetDefaultWarnings(); + optionsBuilder.UseBulkInsertMySql(); optionsBuilder.UseMySql(connectionString, version, options => { options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig index 1e79455e5..fb1b88333 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig @@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none # MA0048: File name must match type name dotnet_diagnostic.MA0048.severity = none +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none + # SA1633: File must have header dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs index d68daddd9..ca7c32d12 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs index 9a8fd9e8d..9f25c5cee 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -15,4 +16,10 @@ public class PostgresAppDbContext(DbContextOptions options, IJsonSerializer json : AppDbContext(options, jsonSerializer) { public override SqlDialect Dialect => PostgresDialect.Instance; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseBulkInsertPostgreSql(); + base.OnConfiguring(optionsBuilder); + } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/.editorconfig index 1e79455e5..fb1b88333 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/.editorconfig +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/.editorconfig @@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none # MA0048: File name must match type name dotnet_diagnostic.MA0048.severity = none +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none + # SA1633: File must have header dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs index 548ef7f0a..01715bd80 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -21,6 +22,7 @@ public sealed class PostgresContentDbContext(string prefix, string connectionStr protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + optionsBuilder.UseBulkInsertPostgreSql(); optionsBuilder.SetDefaultWarnings(); optionsBuilder.UseNpgsql(connectionString, options => { diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/CustomMigrations/AddFlows_Before.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/CustomMigrations/AddFlows_Before.cs index 09db45af3..9a853a974 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/CustomMigrations/AddFlows_Before.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/CustomMigrations/AddFlows_Before.cs @@ -17,15 +17,15 @@ internal class AddFlows_Before : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.DropPrimaryKey( - name: "PK_MessagingData", - table: "MessagingData"); + "PK_MessagingData", + "MessagingData"); migrationBuilder.DropPrimaryKey( - name: "PK_Chats", - table: "Chats"); + "PK_Chats", + "Chats"); migrationBuilder.DropPrimaryKey( - name: "PK_AssetKeyValueStore_TusMetadata", - table: "AssetKeyValueStore_TusMetadata"); + "PK_AssetKeyValueStore_TusMetadata", + "AssetKeyValueStore_TusMetadata"); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig index 1e79455e5..fb1b88333 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig @@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none # MA0048: File name must match type name dotnet_diagnostic.MA0048.severity = none +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none + # SA1633: File must have header dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs index 781bf6077..8ca29e48c 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs index 5b69cfcb2..6366dd279 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -15,4 +16,10 @@ public sealed class SqlServerAppDbContext(DbContextOptions options, IJsonSeriali : AppDbContext(options, jsonSerializer) { public override SqlDialect Dialect => SqlServerDialect.Instance; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseBulkInsertSqlServer(); + base.OnConfiguring(optionsBuilder); + } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/.editorconfig index 1e79455e5..fb1b88333 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/.editorconfig +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/.editorconfig @@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none # MA0048: File name must match type name dotnet_diagnostic.MA0048.severity = none +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none + # SA1633: File must have header dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs index aa4a61870..aea3bd742 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -22,6 +23,7 @@ public sealed class SqlServerContentDbContext(string prefix, string connectionSt protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.SetDefaultWarnings(); + optionsBuilder.UseBulkInsertSqlServer(); optionsBuilder.UseSqlServer(connectionString, options => { options.MigrationsHistoryTable($"{prefix}MigrationHistory"); diff --git a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs index fd0a300e5..a2c531705 100644 --- a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs @@ -36,6 +36,8 @@ 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.Events.EntityFramework; +using Squidex.Flows.EntityFramework; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Log; @@ -226,6 +228,9 @@ public static class ServiceExtensions services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As().As(); + services.AddFlowsCore() .AddEntityFrameworkStore(); diff --git a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj index 0a1665702..79b1f63d5 100644 --- a/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj +++ b/backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj @@ -25,29 +25,28 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - - - + + + + + + - - - - - - - - - - - + + + + + + + diff --git a/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj b/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj index 9557b8c59..4311fe01a 100644 --- a/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj +++ b/backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj @@ -25,12 +25,12 @@ - - - - - - + + + + + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 76be3ff4c..577fc4a4c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -20,7 +20,7 @@ - + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 228de9d00..3d8467ce7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -29,8 +29,8 @@ - - + + diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 508c50768..a46d1aeb5 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -24,13 +24,13 @@ - - - - - - - + + + + + + + diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index 96f11c192..d7b9ae457 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -77,6 +77,7 @@ public sealed class TranslationsController(ICommandBus commandBus, IAssetStore a { var chatRequest = new ChatRequest { + LoadHistory = true, Configuration = request.Configuration, ConversationId = request.ConversationId, Prompt = request.Prompt, @@ -105,6 +106,10 @@ public sealed class TranslationsController(ICommandBus commandBus, IAssetStore a case ToolEndEvent toolEnd: json = new { type = "ToolEnd", tool = toolEnd.Tool.Spec.DisplayName }; break; + case ChatHistoryLoaded historyLoaded: + var message = historyLoaded.Message; + json = new { type = "History", content = message.Content, source = message.Type.ToString() }; + break; } if (json != null) diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 09759e1ed..dc188a9dc 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -59,17 +59,17 @@ - - - - - - + + + + + + - - - - + + + + @@ -83,11 +83,11 @@ - + - + diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar b/backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar index fa88728ed..38aad16f8 100644 --- a/backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar @@ -2,27 +2,30 @@ // Auto-generated code namespace Squidex.EntityFramework.TestHelpers; -[CollectionDefinition("Postgres{{Name}}")] +[CollectionDefinition(Name)] public sealed class Postgres{{Name}}FixtureCollection : ICollectionFixture { + public const string Name = "Postgres{{Name}}"; } public sealed class Postgres{{Name}}Fixture() : PostgresFixture("squidex-postgres-{{Label}}") { } -[CollectionDefinition("MySql{{Name}}")] +[CollectionDefinition(Name)] public sealed class MySql{{Name}}FixtureCollection : ICollectionFixture { + public const string Name = "MySql{{Name}}"; } public sealed class MySql{{Name}}Fixture() : MySqlFixture("squidex-mysql-{{Label}}") { } -[CollectionDefinition("SqlServer{{Name}}")] +[CollectionDefinition(Name)] public sealed class SqlServer{{Name}}FixtureCollection : ICollectionFixture { + public const string Name = "SqlServer{{Name}}"; } public sealed class SqlServer{{Name}}Fixture() : SqlServerFixture("squidex-mssql-{{Label}}") diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar b/backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar index 1b4395889..c7295e1d0 100644 --- a/backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar @@ -11,37 +11,37 @@ namespace {{classNamespace}}; {{#if HasContentContext}} [Trait("Category", "TestContainer")] -[Collection("Postgres{{CollectionSuffix}}")] +[Collection(Postgres{{CollectionSuffix}}FixtureCollection.Name)] public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}(fixture) { } [Trait("Category", "TestContainer")] -[Collection("MySql{{CollectionSuffix}}")] +[Collection(MySql{{CollectionSuffix}}FixtureCollection.Name)] public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}(fixture) { } [Trait("Category", "TestContainer")] -[Collection("SqlServer{{CollectionSuffix}}")] +[Collection(SqlServer{{CollectionSuffix}}FixtureCollection.Name)] public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}(fixture) { } {{else}} [Trait("Category", "TestContainer")] -[Collection("Postgres{{CollectionSuffix}}")] +[Collection(Postgres{{CollectionSuffix}}FixtureCollection.Name)] public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}(fixture) { } [Trait("Category", "TestContainer")] -[Collection("MySql{{CollectionSuffix}}")] +[Collection(MySql{{CollectionSuffix}}FixtureCollection.Name)] public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}(fixture) { } [Trait("Category", "TestContainer")] -[Collection("SqlServer{{CollectionSuffix}}")] +[Collection(SqlServer{{CollectionSuffix}}FixtureCollection.Name)] public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}(fixture) { } diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/BulkHelper.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/BulkHelper.cs deleted file mode 100644 index d9310f317..000000000 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/BulkHelper.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using EFCore.BulkExtensions.SqlAdapters; -using EFCore.BulkExtensions.SqlAdapters.MySql; -using EFCore.BulkExtensions.SqlAdapters.PostgreSql; -using EFCore.BulkExtensions.SqlAdapters.SqlServer; -using Squidex.Providers.MySql.Content; -using Squidex.Providers.Postgres.Content; -using Squidex.Providers.SqlServer.Content; - -namespace Squidex.EntityFramework.TestHelpers; - -public static class BulkHelper -{ - private static readonly IDbServer MySql = new MySqlDbServer(); - private static readonly IDbServer PostgreSql = new PostgreSqlDbServer(); - private static readonly IDbServer SqlServer = new SqlServerDbServer(); - - public static void Configure() - { - SqlAdaptersMapping.Provider = context => - { - switch (context) - { - case MySqlContentDbContext: - return MySql; - case PostgresContentDbContext: - return PostgreSql; - case SqlServerContentDbContext: - return SqlServer; - case TestDbContextMySql: - return MySql; - case TestDbContextPostgres: - return PostgreSql; - case TestDbContextSqlServer: - return SqlServer; - } - - throw new ArgumentException("Not supported.", nameof(context)); - }; - } -} diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs index 111edd4d7..9503a3570 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs @@ -34,11 +34,6 @@ public class MySqlFixture(string? reuseId = null) : IAsyncLifetime, ISqlContentF public IDbContextNamedFactory DbContextNamedFactory => services.GetRequiredService>(); - static MySqlFixture() - { - BulkHelper.Configure(); - } - public async Task InitializeAsync() { await mysql.StartAsync(); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs index 5c6a9f983..0d20e67e5 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs @@ -34,11 +34,6 @@ public class PostgresFixture(string? reuseId) : IAsyncLifetime, ISqlContentFixtu public IDbContextNamedFactory DbContextNamedFactory => services.GetRequiredService>(); - static PostgresFixture() - { - BulkHelper.Configure(); - } - public async Task InitializeAsync() { await postgreSql.StartAsync(); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs index b2ecf901f..0da509b79 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs @@ -35,11 +35,6 @@ public class SqlServerFixture(string? reuseId = null) : IAsyncLifetime, ISqlCont public IDbContextNamedFactory DbContextNamedFactory => services.GetRequiredService>(); - static SqlServerFixture() - { - BulkHelper.Configure(); - } - public async Task InitializeAsync() { await sqlServer.StartAsync(); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs index bde5236bf..68edb0512 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs @@ -6,6 +6,9 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; +using PhenX.EntityFrameworkCore.BulkInsert.MySql; +using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; +using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -24,18 +27,36 @@ public class TestDbContextMySql(DbContextOptions options, IJsonSerializer jsonSe : TestDbContext(options, jsonSerializer) { public override SqlDialect Dialect => MySqlDialect.Instance; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseBulkInsertMySql(); + base.OnConfiguring(optionsBuilder); + } } public class TestDbContextPostgres(DbContextOptions options, IJsonSerializer jsonSerializer) : TestDbContext(options, jsonSerializer) { public override SqlDialect Dialect => PostgresDialect.Instance; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseBulkInsertPostgreSql(); + base.OnConfiguring(optionsBuilder); + } } public class TestDbContextSqlServer(DbContextOptions options, IJsonSerializer jsonSerializer) : TestDbContext(options, jsonSerializer) { public override SqlDialect Dialect => SqlServerDialect.Instance; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseBulkInsertSqlServer(); + base.OnConfiguring(optionsBuilder); + } } public abstract class TestDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Apps/MongoAppRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Apps/MongoAppRepositoryTests.cs index 42698a421..cb41109b5 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Apps/MongoAppRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Apps/MongoAppRepositoryTests.cs @@ -13,7 +13,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Apps; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoAppRepositoryTests(MongoFixture fixture) : AppRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetFolderRepositorySnapshotTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetFolderRepositorySnapshotTests.cs index fac5b7c80..686ade484 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetFolderRepositorySnapshotTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetFolderRepositorySnapshotTests.cs @@ -14,7 +14,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Assets; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoAssetFolderRepositorySnapshotTests(MongoFixture fixture) : AssetFolderSnapshotStoreTests { protected override async Task> CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositorySnapshotTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositorySnapshotTests.cs index 20d9e4bf1..4f0bc989b 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositorySnapshotTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositorySnapshotTests.cs @@ -15,7 +15,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Assets; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoAssetRepositorySnapshotTests(MongoFixture fixture) : AssetSnapshotStoreTests { protected override async Task> CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositoryTests.cs index ab7e6f1cd..64b6b7d0b 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositoryTests.cs @@ -14,7 +14,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Assets; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoAssetRepositoryTests(MongoFixture fixture) : AssetRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryDedicatedTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryDedicatedTests.cs index b987157d5..5ebdeffc9 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryDedicatedTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryDedicatedTests.cs @@ -16,7 +16,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Contents; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoContentRepositoryDedicatedTests(MongoFixture fixture) : ContentRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotDedicatedTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotDedicatedTests.cs index 2d656b2a7..bc7c28e37 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotDedicatedTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotDedicatedTests.cs @@ -17,7 +17,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Contents; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoContentRepositorySnapshotDedicatedTests(MongoFixture fixture) : ContentSnapshotStoreTests { protected override bool CheckConsistencyOnWrite => false; diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotTests.cs index 05f26bab8..2ee6b9e12 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotTests.cs @@ -17,7 +17,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Contents; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoContentRepositorySnapshotTests(MongoFixture fixture) : ContentSnapshotStoreTests { protected override bool CheckConsistencyOnWrite => false; diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryTests.cs index e5f03de85..3c3b77c52 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryTests.cs @@ -16,7 +16,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Contents; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoContentRepositoryTests(MongoFixture fixture) : ContentRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs index e41d53db6..9084fb7b2 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs @@ -11,7 +11,7 @@ using Squidex.MongoDb.TestHelpers; namespace Squidex.MongoDb.Domain.Contents.Text; [Trait("Category", "Dependencies")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoTextIndexTests(MongoFixture fixture) : TextIndexerTests { public override bool SupportsQuerySyntax => false; diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs index d352a4477..add65d99e 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs @@ -14,7 +14,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Contents.Text; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoTextIndexerStateTests(MongoFixture fixture) : TextIndexerStateTests { protected override async Task CreateSutAsync(IContentRepository contentRepository) diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/History/MongoHistoryEventRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/History/MongoHistoryEventRepositoryTests.cs index 932a57706..6dbe71e6a 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/History/MongoHistoryEventRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/History/MongoHistoryEventRepositoryTests.cs @@ -13,7 +13,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.History; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoHistoryEventRepositoryTests(MongoFixture fixture) : HistoryEventRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Rules/MongoRuleRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Rules/MongoRuleRepositoryTests.cs index 743e8faa0..254ff9491 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Rules/MongoRuleRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Rules/MongoRuleRepositoryTests.cs @@ -13,7 +13,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Rules; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoRuleRepositoryTests(MongoFixture fixture) : RuleRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemaRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemaRepositoryTests.cs index f4a07cb26..c8e39c50b 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemaRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemaRepositoryTests.cs @@ -13,7 +13,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Schemas; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoSchemaRepositoryTests(MongoFixture fixture) : SchemaRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemasHashTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemasHashTests.cs index 6efb0cea7..44b488cda 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemasHashTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemasHashTests.cs @@ -19,7 +19,7 @@ using Squidex.MongoDb.TestHelpers; namespace Squidex.MongoDb.Domain.Schemas; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoSchemasHashTests(MongoFixture fixture) : GivenContext, IAsyncLifetime { private readonly MongoSchemasHash sut = new MongoSchemasHash(fixture.Database); diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Teams/MongoTeamRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Teams/MongoTeamRepositoryTests.cs index a6d27a3f6..a1eeefcbd 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Teams/MongoTeamRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Teams/MongoTeamRepositoryTests.cs @@ -13,7 +13,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Domain.Teams; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoTeamRepositoryTests(MongoFixture fixture) : TeamRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Caching/MongoDistributedCacheTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Caching/MongoDistributedCacheTests.cs index 0f6c0248c..e7d8372c1 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Caching/MongoDistributedCacheTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Caching/MongoDistributedCacheTests.cs @@ -13,7 +13,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Infrastructure.Caching; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoDistributedCacheTests(MongoFixture fixture) : DistributedCacheTests { protected override async Task CreateSutAsync(TimeProvider timeProvider) diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Log/MongoRequestLogRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Log/MongoRequestLogRepositoryTests.cs index 01a42e317..014b8429f 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Log/MongoRequestLogRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Log/MongoRequestLogRepositoryTests.cs @@ -13,7 +13,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Infrastructure.Log; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoRequestLogRepositoryTests(MongoFixture fixture) : RequestLogRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Migrations/MongoMigrationStatusTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Migrations/MongoMigrationStatusTests.cs index f164e787c..033a9c051 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Migrations/MongoMigrationStatusTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Migrations/MongoMigrationStatusTests.cs @@ -12,7 +12,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Infrastructure.Migrations; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoMigrationStatusTests(MongoFixture fixture) : MigrationStatusTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/States/MongoSnapshotStoreTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/States/MongoSnapshotStoreTests.cs index f10b2d320..d020d838d 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/States/MongoSnapshotStoreTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/States/MongoSnapshotStoreTests.cs @@ -12,7 +12,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Infrastructure.States; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoSnapshotStoreTests(MongoFixture fixture) : SnapshotStoreTests { protected override async Task> CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/UsageTracking/MongoUsageRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/UsageTracking/MongoUsageRepositoryTests.cs index 30d93f679..c8dbc85b3 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/UsageTracking/MongoUsageRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/UsageTracking/MongoUsageRepositoryTests.cs @@ -12,7 +12,7 @@ using Squidex.Shared; namespace Squidex.MongoDb.Infrastructure.UsageTracking; [Trait("Category", "TestContainer")] -[Collection("Mongo")] +[Collection(MongoFixtureCollection.Name)] public class MongoUsageRepositoryTests(MongoFixture fixture) : UsageRepositoryTests { protected override async Task CreateSutAsync() diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/MongoFixture.cs b/backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/MongoFixture.cs index f86ac5c69..22e518307 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/MongoFixture.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/MongoFixture.cs @@ -12,9 +12,10 @@ using Testcontainers.MongoDb; namespace Squidex.MongoDb.TestHelpers; -[CollectionDefinition("Mongo")] +[CollectionDefinition(Name)] public sealed class MongoFixtureCollection : ICollectionFixture { + public const string Name = "Mongo"; } public class MongoFixture : IAsyncLifetime diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs index a44fb8d77..daa658abb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs @@ -439,11 +439,10 @@ public abstract class TextIndexerTests : GivenContext { var query = new GeoQuery(SchemaId.Id, field, latitude, longitude, 1000, 1000) { - SchemaId = default, + SchemaId = SchemaId.Id, }; var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected)); - AssertIds(actual, expected); } @@ -455,7 +454,6 @@ public abstract class TextIndexerTests : GivenContext }; var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected)); - AssertIds(actual, expected); } @@ -467,7 +465,6 @@ public abstract class TextIndexerTests : GivenContext }; var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected)); - AssertIds(actual, expected); } diff --git a/frontend/src/app/features/apps/pages/onboarding-dialog.component.html b/frontend/src/app/features/apps/pages/onboarding-dialog.component.html index 9ec8d0c52..878b13cb1 100644 --- a/frontend/src/app/features/apps/pages/onboarding-dialog.component.html +++ b/frontend/src/app/features/apps/pages/onboarding-dialog.component.html @@ -12,7 +12,7 @@ {{ "tour.welcome" | sqxTranslate }} {{ "tour.welcomeProduct" | sqxTranslate }} -
+
@@ -34,15 +34,10 @@
@@ -51,13 +46,9 @@ @@ -66,17 +57,11 @@ diff --git a/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html b/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html index 54539851d..1340564e5 100644 --- a/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html +++ b/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html @@ -62,7 +62,7 @@ replaceUrl="true" routerLink="history" routerLinkActive="active" - sqxTourStep="help" + sqxTourStep="history" title="i18n:common.history" titlePosition="left"> diff --git a/frontend/src/app/framework/angular/modals/tour-step.directive.ts b/frontend/src/app/framework/angular/modals/tour-step.directive.ts index 17d18ec0d..2a7030ed8 100644 --- a/frontend/src/app/framework/angular/modals/tour-step.directive.ts +++ b/frontend/src/app/framework/angular/modals/tour-step.directive.ts @@ -16,6 +16,7 @@ import { StepDefinition, TourService } from './tour.service'; export class TourStepDirective implements OnInit, OnDestroy, TourAnchorDirective { private isNextOnClick = false; private isActive = false; + private currentAnchorId?: string | null; private wasClicked = false; @Input({ alias: 'sqxTourStep', required: true }) @@ -27,28 +28,30 @@ export class TourStepDirective implements OnInit, OnDestroy, TourAnchorDirective } public ngOnInit(): void { - if (!this.anchorId) { + this.currentAnchorId = this.anchorId; + + if (!this.currentAnchorId) { return; } - this.tourService.register(this.anchorId, this); + this.tourService.register(this.currentAnchorId, this); } public ngOnDestroy(): void { - if (!this.anchorId) { + if (!this.currentAnchorId) { return; } if (this.isActive && (!this.isNextOnClick || !this.wasClicked)) { setTimeout(() => { - if (this.tourService.currentStep.anchorId === this.anchorId) { + if (this.tourService.currentStep.anchorId === this.currentAnchorId) { this.tourService.render(null, null); this.tourService.pause(); } }, 200); } - this.tourService.unregister(this.anchorId); + this.tourService.unregister(this.currentAnchorId); } @HostListener('click') diff --git a/frontend/src/app/framework/angular/modals/tour-template.component.html b/frontend/src/app/framework/angular/modals/tour-template.component.html index 35b7d7196..5f726f63b 100644 --- a/frontend/src/app/framework/angular/modals/tour-template.component.html +++ b/frontend/src/app/framework/angular/modals/tour-template.component.html @@ -19,7 +19,7 @@
-
+
diff --git a/frontend/src/app/framework/angular/modals/tour.service.ts b/frontend/src/app/framework/angular/modals/tour.service.ts index 157a58569..366d8213e 100644 --- a/frontend/src/app/framework/angular/modals/tour.service.ts +++ b/frontend/src/app/framework/angular/modals/tour.service.ts @@ -6,7 +6,7 @@ */ import { Injectable } from '@angular/core'; -import { TourService as BaseTourService, IStepOption, TourState } from 'ngx-ui-tour-core'; +import { TourService as BaseTourService, IStepOption, TourAnchorDirective, TourState } from 'ngx-ui-tour-core'; import { filter, Observable, Subscription, take } from 'rxjs'; import { FloatingPlacement } from '@app/framework/internal'; import { TourTemplateComponent } from './tour-template.component'; @@ -21,8 +21,11 @@ export interface StepDefinition extends IStepOption { // Additional callback. hideThis?: () => void; + // Goes to the end automatically. + endOnCondition?: ((service: TourService, anchor: TourAnchorDirective) => Observable) | null; + // Goes to the next element automatically. - nextOnCondition?: ((service: TourService) => Observable) | null; + nextOnCondition?: ((service: TourService, anchor: TourAnchorDirective) => Observable) | null; } export function waitForAnchor(anchorId: string) { @@ -34,11 +37,69 @@ export function waitForAnchor(anchorId: string) { }; } +export function waitForAnchorClick() { + return (_: TourService, anchor: TourAnchorDirective) => { + return new Observable(subscriber => { + const element = anchor.element.nativeElement as HTMLElement; + + const listener = () => { + subscriber.next(true); + subscriber.complete(); + element.removeEventListener('click', listener); + }; + + element.addEventListener('click', listener); + + return () => { + element.removeEventListener('click', listener); + }; + }); + }; +} + +export function waitForElement(selector: string) { + return () => { + return new Observable(subscriber => { + const observer = new MutationObserver((mutationsList) => { + let shouldUpdate = false; + + for (const mutation of mutationsList) { + if (mutation.type === 'childList' || mutation.type === 'attributes') { + shouldUpdate = true; + break; + } + } + + if (shouldUpdate) { + const element = document.querySelector(selector); + if (element) { + subscriber.next(true); + subscriber.complete(); + observer.disconnect(); + } + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'], + }); + + return () => { + observer.disconnect(); + }; + }); + }; +} + @Injectable({ providedIn: 'root', }) export class TourService extends BaseTourService { - private condition?: Subscription; + private onNext?: Subscription; + private onEnd?: Subscription; public component?: TourTemplateComponent | null = null; @@ -55,10 +116,24 @@ export class TourService extends BaseTourService { document.body.style.overflow = 'auto'; }); + this.stepShow$ + .subscribe(({ step }) => { + const directive = this.anchors[step.anchorId!]; + + this.onNext = step.nextOnCondition?.(this, directive)?.subscribe(() => { + this.goto(this.steps.indexOf(step) + 1); + }); + + this.onEnd = step.endOnCondition?.(this, directive)?.subscribe(() => { + this.end(); + }); + }); + this.stepHide$ .subscribe(() => { if (this.getStatus() !== TourState.PAUSED) { - this.condition?.unsubscribe(); + this.onNext?.unsubscribe(); + this.onEnd?.unsubscribe(); } }); } @@ -66,17 +141,4 @@ export class TourService extends BaseTourService { public render(step: StepDefinition | null, target: any | null) { this.component?.render(step, target); } - - public run(steps: StepDefinition[]) { - this.initialize(steps); - this.start(); - } - - protected showStep(step: StepDefinition): Promise { - this.condition = step.nextOnCondition?.(this)?.subscribe(() => { - this.goto(this.steps.indexOf(step) + 1); - }); - - return super.showStep(step); - } } \ No newline at end of file diff --git a/frontend/src/app/shared/components/chat-dialog.component.ts b/frontend/src/app/shared/components/chat-dialog.component.ts index 0bb727a04..b96aae861 100644 --- a/frontend/src/app/shared/components/chat-dialog.component.ts +++ b/frontend/src/app/shared/components/chat-dialog.component.ts @@ -8,7 +8,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { Observable } from 'rxjs'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; import { HTTP, MathHelper, ModalDialogComponent, TooltipDirective, TranslatePipe } from '@app/framework'; import { AppsState, AuthService, ChatEventDto, StatefulComponent, TranslationsService } from '@app/shared/internal'; import { ChatItemComponent } from './chat-item.component'; @@ -21,7 +21,7 @@ interface State { isRunning: boolean; // The answers. - chatItems: ReadonlyArray<{ content: string | Observable; type: 'User' | 'Bot' | 'System' }>; + chatItems: ReadonlyArray<{ content: string | Observable; type: 'User' | 'Assistant' | 'System' }>; } @Component({ @@ -38,14 +38,15 @@ interface State { ], }) export class ChatDialogComponent extends StatefulComponent { - private readonly conversationId = MathHelper.guid(); - @Output() public contentSelect = new EventEmitter(); @Input() public configuration?: string; + @Input() + public conversationId = MathHelper.guid(); + @Input() public folderId?: string; @@ -71,14 +72,32 @@ export class ChatDialogComponent extends StatefulComponent { public ngOnInit() { const { configuration, conversationId } = this; - const stream = this.translator.ask(this.appsState.appName, { conversationId, configuration }); - this.next(s => ({ - ...s, - chatQuestion: '', - chatItems: [...s.chatItems, { content: stream, type: 'Bot' }], - isRunning: true, - })); + let observable: Subject; + this.translator.ask(this.appsState.appName, { conversationId, configuration }) + .subscribe(message => { + if (message.type === 'History') { + const { content, source } = message; + + this.next(s => ({ + ...s, + chatItems: [...s.chatItems, { content, type: source }], + })); + } else { + if (!observable) { + observable = new ReplaySubject(); + + this.next(s => ({ + ...s, + chatQuestion: '', + chatItems: [...s.chatItems, { content: observable, type: 'Assistant' }], + isRunning: true, + })); + } + + observable.next(message); + } + }); } public setQuestion(chatQuestion: string) { @@ -105,7 +124,7 @@ export class ChatDialogComponent extends StatefulComponent { chatItems: [ ...s.chatItems, { content: prompt, type: 'User' }, - { content: stream, type: 'Bot' }, + { content: stream, type: 'Assistant' }, ], isRunning: true, })); diff --git a/frontend/src/app/shared/components/chat-item.component.html b/frontend/src/app/shared/components/chat-item.component.html index dfb00b28c..66cb6a241 100644 --- a/frontend/src/app/shared/components/chat-item.component.html +++ b/frontend/src/app/shared/components/chat-item.component.html @@ -20,7 +20,7 @@ } -@if (type === "Bot") { +@if (type === "Assistant") {
@@ -45,7 +45,7 @@ {{ "chat.failed" | sqxTranslate }} } - @if (!snapshot.isRunning && !isFirst && type === "Bot") { + @if (!snapshot.isRunning && !isFirst && type === "Assistant") {
- @if (snapshot.isRunning) { + @if (snapshot.isRunning && !snapshot.content) { - - } diff --git a/frontend/src/app/shared/components/chat-item.component.ts b/frontend/src/app/shared/components/chat-item.component.ts index a380a7924..4742ae135 100644 --- a/frontend/src/app/shared/components/chat-item.component.ts +++ b/frontend/src/app/shared/components/chat-item.component.ts @@ -49,7 +49,7 @@ export class ChatItemComponent extends StatefulComponent { public focusElement!: ElementRef; @Input({ required: true }) - public type: 'Bot' | 'User' | 'System' = 'Bot'; + public type: 'Assistant' | 'User' | 'System' = 'Assistant'; @Input({ required: true }) public folderId?: string; diff --git a/frontend/src/app/shared/components/tour-guide.component.html b/frontend/src/app/shared/components/tour-guide.component.html index ed1552ae8..00c0f873d 100644 --- a/frontend/src/app/shared/components/tour-guide.component.html +++ b/frontend/src/app/shared/components/tour-guide.component.html @@ -45,18 +45,14 @@
-
{{ "tour.documentation" | sqxTranslate }}
-
-
{{ "tour.support" | sqxTranslate }}
-
diff --git a/frontend/src/app/shared/components/tour-guide.component.scss b/frontend/src/app/shared/components/tour-guide.component.scss index 85a4697df..e1d2f2bea 100644 --- a/frontend/src/app/shared/components/tour-guide.component.scss +++ b/frontend/src/app/shared/components/tour-guide.component.scss @@ -77,6 +77,7 @@ $caret-size: 14px; .summary { align-items: center; color: $color-white; + cursor: pointer; display: inline-flex; flex-direction: row; flex-wrap: nowrap; diff --git a/frontend/src/app/shared/components/tour-guide.component.ts b/frontend/src/app/shared/components/tour-guide.component.ts index 8560b0df7..45ede2188 100644 --- a/frontend/src/app/shared/components/tour-guide.component.ts +++ b/frontend/src/app/shared/components/tour-guide.component.ts @@ -63,10 +63,6 @@ export class TourGuideComponent extends StatefulComponent implements OnIn } public start(task: TaskSnapshot) { - if (!task.isActive) { - return; - } - this.tourState.runTask(task); } } diff --git a/frontend/src/app/shared/services/rules.service.ts b/frontend/src/app/shared/services/rules.service.ts index 078878d78..f695a1054 100644 --- a/frontend/src/app/shared/services/rules.service.ts +++ b/frontend/src/app/shared/services/rules.service.ts @@ -180,7 +180,7 @@ export class RulesService { const url = this.apiUrl.buildUrl(link.href); - return this.http.request(link.method, url, {}).pipe( + return this.http.request(link.method, url, { body: {} }).pipe( pretifyError('i18n:rules.triggerFailed')); } diff --git a/frontend/src/app/shared/services/translations.service.ts b/frontend/src/app/shared/services/translations.service.ts index 1b1636cbe..fa029d939 100644 --- a/frontend/src/app/shared/services/translations.service.ts +++ b/frontend/src/app/shared/services/translations.service.ts @@ -31,6 +31,16 @@ export interface ChatChunkDto { content: string; } +export interface ChatHistoryLoaded { + type: 'History'; + + // The content of the chunk. + content: string; + + // Defines where the message was created from. + source: 'User' | 'Assistant'; +} + export interface ChatToolStartDto { type: 'ToolStart'; @@ -45,7 +55,7 @@ export interface ChatToolEndDto { tool: string; } -export type ChatEventDto = ChatChunkDto | ChatToolStartDto | ChatToolEndDto; +export type ChatEventDto = ChatChunkDto | ChatToolStartDto | ChatToolEndDto | ChatHistoryLoaded; @Injectable({ diff --git a/frontend/src/app/shared/state/tour.state.ts b/frontend/src/app/shared/state/tour.state.ts index 3612ef24d..05cff8569 100644 --- a/frontend/src/app/shared/state/tour.state.ts +++ b/frontend/src/app/shared/state/tour.state.ts @@ -96,7 +96,8 @@ export class TourState extends State { return; } - this.tourService.run(task.steps); + this.tourService.initialize(task.steps); + this.tourService.start(); } public disableAllHints() { diff --git a/frontend/src/app/shared/state/tour.tasks.ts b/frontend/src/app/shared/state/tour.tasks.ts index 038928472..12b280ab6 100644 --- a/frontend/src/app/shared/state/tour.tasks.ts +++ b/frontend/src/app/shared/state/tour.tasks.ts @@ -7,7 +7,7 @@ import { inject, InjectionToken } from '@angular/core'; import { filter, Observable, take } from 'rxjs'; -import { MessageBus, StepDefinition, waitForAnchor } from '@app/framework'; +import { MessageBus, StepDefinition, waitForAnchor, waitForAnchorClick, waitForElement } from '@app/framework'; import { ClientTourStated, QueryExecuted } from '../utils/messages'; import { AppsState } from './apps.state'; import { AssetsState } from './assets.state'; @@ -128,11 +128,13 @@ export function buildTasks() { }, { anchorId: 'help', content: 'i18n:tour.createSchema.helpContent', + endOnCondition: waitForElement('.panel2.minimized .right'), scrollContainer: '.panel-container', position: 'left-start', }, { anchorId: 'history', content: 'i18n:tour.createSchema.historyContent', + endOnCondition: waitForElement('.panel2.minimized .right'), scrollContainer: '.panel-container', position: 'left-start', }], @@ -183,6 +185,8 @@ export function buildTasks() { }, { anchorId: 'status', content: 'i18n:tour.createContent.statusContent', + endOnCondition: waitForAnchorClick(), + nextOnAnchorClick: true, isAsync: true, position: 'left-start', }], @@ -221,6 +225,7 @@ export function buildTasks() { anchorId: 'filter', content: 'i18n:tour.createAsset.filterContent', position: 'left-start', + endOnCondition: waitForElement('.panel2.minimized .right'), }], onComplete: (() => { const assetsState = inject(AssetsState); diff --git a/frontend/src/app/shell/pages/internal/chat-menu.component.html b/frontend/src/app/shell/pages/internal/chat-menu.component.html index ade9fd897..c0ff0e09f 100644 --- a/frontend/src/app/shell/pages/internal/chat-menu.component.html +++ b/frontend/src/app/shell/pages/internal/chat-menu.component.html @@ -3,4 +3,4 @@ } - + diff --git a/frontend/src/app/shell/pages/internal/chat-menu.component.ts b/frontend/src/app/shell/pages/internal/chat-menu.component.ts index c944b3ef9..99cde8b53 100644 --- a/frontend/src/app/shell/pages/internal/chat-menu.component.ts +++ b/frontend/src/app/shell/pages/internal/chat-menu.component.ts @@ -7,7 +7,7 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { AppsState, ChatDialogComponent, DialogModel, ModalDirective, UIOptions } from '@app/shared'; +import { AppsState, AuthService, ChatDialogComponent, DialogModel, ModalDirective, UIOptions } from '@app/shared'; @Component({ standalone: true, @@ -28,6 +28,7 @@ export class ChatMenuComponent { constructor( public readonly appsState: AppsState, + public readonly authService: AuthService, ) { } } diff --git a/frontend/src/app/theme/_bootstrap.scss b/frontend/src/app/theme/_bootstrap.scss index 2d6af8824..0bd06c013 100644 --- a/frontend/src/app/theme/_bootstrap.scss +++ b/frontend/src/app/theme/_bootstrap.scss @@ -70,6 +70,10 @@ a { color: inherit; } + + ul { + margin: 0; + } } .alert-hint { diff --git a/frontend/src/app/theme/_common.scss b/frontend/src/app/theme/_common.scss index a7fb5b324..90646e539 100644 --- a/frontend/src/app/theme/_common.scss +++ b/frontend/src/app/theme/_common.scss @@ -17,6 +17,10 @@ body { } } +html { + height: 100vh; +} + hr { border-color: $color-border; }