From bab6d23cdfa62e501cdab71534b23a825e759e90 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 26 Mar 2024 07:59:07 +0100 Subject: [PATCH] Update dependencies. (#1082) * Update dependencies. * Use new cache settings. * Fix tests. * Fix domain object cache.- * Test subscriptions again. * Disable messaging cache. * Use random name for cluster instances. * Bind settings properly. --- .../Squidex.Extensions.csproj | 4 +- backend/src/Migrations/Migrations.csproj | 2 +- .../Squidex.Domain.Apps.Core.Model.csproj | 2 +- .../Extensions/StringAsyncJintExtension.cs | 14 +-- ...Squidex.Domain.Apps.Core.Operations.csproj | 7 +- .../Subscriptions/AssetSubscription.cs | 29 ++---- .../Subscriptions/ContentSubscription.cs | 8 +- .../Subscriptions/EventMessageEvaluator.cs | 86 ----------------- .../Subscriptions/EventMessageWrapper.cs | 12 +-- .../Subscriptions/SubscriptionPublisher.cs | 31 ++++-- ...quidex.Domain.Apps.Entities.MongoDb.csproj | 2 +- .../Apps/AppCacheOptions.cs} | 14 +-- .../Apps/AppPermanentDeleter.cs | 4 +- .../Apps/Indexes/AppsIndex.cs | 27 ++++-- .../Assets/AssetPermanentDeleter.cs | 4 +- .../Assets/RecursiveDeleter.cs | 4 +- .../Backup/RestoreJob.cs | 2 +- .../GraphQL/Types/Assets/AssetActions.cs | 27 +++++- .../GraphQL/Types/Contents/ContentActions.cs | 30 +++++- .../Contents/GraphQL/Types/Resolvers.cs | 26 ----- .../Jobs/JobWorker.cs | 1 - .../Schemas/Indexes/SchemasIndex.cs | 19 +++- .../Schemas/SchemaCacheOptions.cs | 13 +++ .../Squidex.Domain.Apps.Entities.csproj | 2 +- .../Squidex.Domain.Apps.Events.csproj | 2 +- .../Squidex.Domain.Users.MongoDb.csproj | 2 +- .../Squidex.Domain.Users.csproj | 6 +- ...quidex.Infrastructure.GetEventStore.csproj | 8 +- .../Squidex.Infrastructure.MongoDb.csproj | 2 +- .../Commands/DefaultDomainObjectCache.cs | 19 +++- .../Consume/ParseSubscription.cs | 2 +- .../EventSourcing/Envelope{T}.cs | 28 +----- .../EventSourcing/IEventConsumer.cs | 4 +- .../Squidex.Infrastructure.csproj | 16 ++-- .../src/Squidex.Shared/Squidex.Shared.csproj | 2 +- .../Squidex.Web/Pipeline/SetupMiddleware.cs | 1 - backend/src/Squidex.Web/Squidex.Web.csproj | 2 +- .../Api/Config/OpenApi/OpenApiServices.cs | 2 - .../Controllers/Translations/Models/AskDto.cs | 5 + .../Translations/TranslationsController.cs | 16 +--- .../Squidex/Config/Domain/CommandsServices.cs | 30 ++++++ .../Squidex/Config/Domain/FontendServices.cs | 2 +- .../Config/Domain/InfrastructureServices.cs | 15 ++- .../Squidex/Config/Domain/StoreServices.cs | 9 ++ .../Config/Messaging/MessagingServices.cs | 94 +++++++++--------- backend/src/Squidex/Squidex.csproj | 42 ++++---- backend/src/Squidex/appsettings.json | 21 +++- .../Scripting/JintScriptEngineHelperTests.cs | 6 +- .../Subscriptions/AssetSubscriptionTests.cs | 40 ++------ .../Subscriptions/ContentSubscriptionTests.cs | 6 +- .../EventMessageEvaluatorTests.cs | 96 ------------------- .../SubscriptionPublisherTests.cs | 38 ++++++-- .../Squidex.Domain.Apps.Core.Tests.csproj | 4 +- .../AppProviderExtensionsTests.cs | 10 -- .../Apps/AppPermanentDeleterTests.cs | 12 +-- .../Apps/Indexes/AppsIndexTests.cs | 66 ++++++++++--- .../Assets/AssetChangedTriggerHandlerTests.cs | 1 - .../Assets/AssetPermanentDeleterTests.cs | 8 +- .../Assets/ImageAssetMetadataSourceTests.cs | 1 - .../GraphQL/GraphQLSubscriptionTests.cs | 4 +- .../Contents/GraphQL/GraphQLTestBase.cs | 2 +- .../Rules/RuleQueueWriterTests.cs | 1 - .../Guards/GuardSchemaFieldTests.cs | 1 - .../Schemas/Indexes/SchemasIndexTests.cs | 60 +++++++++--- .../Squidex.Domain.Apps.Entities.Tests.csproj | 4 +- .../Squidex.Domain.Users.Tests.csproj | 2 +- .../Commands/DefaultDomainObjectCacheTests.cs | 30 ++++++ .../Consume/EventConsumerProcessorTests.cs | 4 +- .../Queries/QueryFromJsonTests.cs | 1 - .../Squidex.Infrastructure.Tests.csproj | 4 +- .../Squidex.Web.Tests.csproj | 2 +- .../shared/forms/content-field.component.scss | 4 +- .../components/chat-dialog.component.html | 14 ++- .../components/chat-dialog.component.scss | 15 +++ .../components/chat-dialog.component.ts | 7 +- .../shared/services/translations.service.ts | 3 + .../GraphQLSubscriptionTests.cs | 3 - tools/TestSuite/docker-compose-base.yml | 2 + tools/TestSuite/docker-compose.yml | 16 ++++ 79 files changed, 591 insertions(+), 576 deletions(-) delete mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs rename backend/src/{Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs => Squidex.Domain.Apps.Entities/Apps/AppCacheOptions.cs} (50%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCacheOptions.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj index 674e6bb5a..b5f0f9942 100644 --- a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -16,10 +16,10 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Migrations/Migrations.csproj b/backend/src/Migrations/Migrations.csproj index 8fc18e7dc..2aff5d219 100644 --- a/backend/src/Migrations/Migrations.csproj +++ b/backend/src/Migrations/Migrations.csproj @@ -6,7 +6,7 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 32743858a..1702a34ed 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 @@ -12,7 +12,7 @@ True - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs index 11214ae30..86dfe41f0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs @@ -7,8 +7,8 @@ using Jint.Native; using Jint.Runtime; +using Squidex.AI; using Squidex.Domain.Apps.Core.Properties; -using Squidex.Text.ChatBots; using Squidex.Text.Translations; #pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections @@ -61,17 +61,9 @@ public sealed class StringAsyncJintExtension : IJintExtension, IScriptDescriptor return; } - var conversationId = Guid.NewGuid().ToString(); - try - { - var result = await chatAgent.PromptAsync(conversationId, prompt, ct); + var result = await chatAgent.PromptAsync(prompt, ct: ct); - scheduler.Run(callback, JsValue.FromObject(context.Engine, result.Text)); - } - finally - { - await chatAgent.StopConversationAsync(conversationId); - } + scheduler.Run(callback, JsValue.FromObject(context.Engine, result.Text)); } catch (Exception ex) { 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 4803fe9d3..0da4877ec 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 @@ -18,17 +18,18 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs index bdd31a1f6..109331a8d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs @@ -7,15 +7,15 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Events.Assets; -using Squidex.Shared; +using Squidex.Messaging.Subscriptions; namespace Squidex.Domain.Apps.Core.Subscriptions; -public sealed class AssetSubscription : AppSubscription +public sealed class AssetSubscription : ISubscription { - public EnrichedAssetEventType? Type { get; set; } + public EnrichedAssetEventType? Type { get; init; } - public override ValueTask ShouldHandle(object message) + public ValueTask ShouldHandle(object message) { return new ValueTask(ShouldHandleCore(message)); } @@ -25,24 +25,14 @@ public sealed class AssetSubscription : AppSubscription switch (message) { case EnrichedAssetEvent enrichedAssetEvent: - return ShouldHandle(enrichedAssetEvent); + return CheckType(enrichedAssetEvent); case AssetEvent assetEvent: - return ShouldHandle(assetEvent); + return CheckType(assetEvent); default: return false; } } - private bool ShouldHandle(EnrichedAssetEvent @event) - { - return CheckType(@event) && CheckPermission(@event.AppId.Name); - } - - private bool ShouldHandle(AssetEvent @event) - { - return CheckType(@event) && CheckPermission(@event.AppId.Name); - } - private bool CheckType(EnrichedAssetEvent @event) { return Type == null || Type.Value == @event.Type; @@ -64,11 +54,4 @@ public sealed class AssetSubscription : AppSubscription return true; } } - - private bool CheckPermission(string appName) - { - var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, appName); - - return Permissions.Includes(permission); - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs index 052527d70..be8367885 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs @@ -7,17 +7,21 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.Security; +using Squidex.Messaging.Subscriptions; using Squidex.Shared; namespace Squidex.Domain.Apps.Core.Subscriptions; -public sealed class ContentSubscription : AppSubscription +public sealed class ContentSubscription : ISubscription { + public PermissionSet Permissions { get; set; } + public string? SchemaName { get; set; } public EnrichedContentEventType? Type { get; set; } - public override ValueTask ShouldHandle(object message) + public ValueTask ShouldHandle(object message) { return new ValueTask(ShouldHandleCore(message)); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs deleted file mode 100644 index cc641d3c0..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Messaging.Subscriptions; - -namespace Squidex.Domain.Apps.Core.Subscriptions; - -public sealed class EventMessageEvaluator : IMessageEvaluator -{ - private readonly Dictionary> subscriptions = []; - private readonly ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim(); - - public async ValueTask> GetSubscriptionsAsync(object message) - { - if (message is not AppEvent appEvent) - { - return Enumerable.Empty(); - } - - readerWriterLock.EnterReadLock(); - try - { - List? result = null; - - if (subscriptions.TryGetValue(appEvent.AppId.Id, out var appSubscriptions)) - { - foreach (var (id, subscription) in appSubscriptions) - { - if (await subscription.ShouldHandle(appEvent)) - { - result ??= []; - result.Add(id); - } - } - } - - return result ?? Enumerable.Empty(); - } - finally - { - readerWriterLock.ExitReadLock(); - } - } - - public void SubscriptionAdded(Guid id, ISubscription subscription) - { - if (subscription is not AppSubscription appSubscription) - { - return; - } - - readerWriterLock.EnterWriteLock(); - try - { - subscriptions.GetOrAddNew(appSubscription.AppId)[id] = appSubscription; - } - finally - { - readerWriterLock.ExitWriteLock(); - } - } - - public void SubscriptionRemoved(Guid id, ISubscription subscription) - { - if (subscription is not AppSubscription appSubscription) - { - return; - } - - readerWriterLock.EnterWriteLock(); - try - { - subscriptions.GetOrAddDefault(appSubscription.AppId)?.Remove(id); - } - finally - { - readerWriterLock.ExitWriteLock(); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs index 0afc04bba..a97c124a8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs @@ -13,31 +13,29 @@ namespace Squidex.Domain.Apps.Core.Subscriptions; public sealed class EventMessageWrapper : IPayloadWrapper { - private readonly IEnumerable subscriptionEventCreators; + private readonly IEnumerable creators; public Envelope Event { get; } object IPayloadWrapper.Message => Event.Payload; - public EventMessageWrapper(Envelope @event, IEnumerable subscriptionEventCreators) + public EventMessageWrapper(Envelope @event, IEnumerable creators) { Event = @event; - this.subscriptionEventCreators = subscriptionEventCreators; + this.creators = creators; } public async ValueTask CreatePayloadAsync() { - foreach (var creator in subscriptionEventCreators) + foreach (var creator in creators) { if (!creator.Handles(Event.Payload)) { continue; } - var result = await creator.CreateEnrichedEventsAsync(Event, default); - - if (result != null) + if (await creator.CreateEnrichedEventsAsync(Event, default) is object result) { return result; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs index 4bd054ed7..48149a252 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs @@ -6,6 +6,9 @@ // ========================================================================== using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Messaging.Subscriptions; @@ -14,7 +17,7 @@ namespace Squidex.Domain.Apps.Core.Subscriptions; public sealed class SubscriptionPublisher : IEventConsumer { private readonly ISubscriptionService subscriptionService; - private readonly IEnumerable subscriptionEventCreators; + private readonly IEnumerable subscriptionCreators; public string Name => "Subscriptions"; @@ -24,26 +27,36 @@ public sealed class SubscriptionPublisher : IEventConsumer public bool CanClear => false; - public SubscriptionPublisher(ISubscriptionService subscriptionService, IEnumerable subscriptionEventCreators) + public SubscriptionPublisher(ISubscriptionService subscriptionService, + IEnumerable subscriptionCreators) { this.subscriptionService = subscriptionService; - this.subscriptionEventCreators = subscriptionEventCreators; + this.subscriptionCreators = subscriptionCreators; } - public bool Handles(StoredEvent @event) + public async ValueTask HandlesAsync(StoredEvent @event) { - return subscriptionService.HasSubscriptions; + var key = @event.StreamName.Split(DomainId.IdSeparator)[0]; + + return await subscriptionService.HasSubscriptionsAsync(key); } public Task On(Envelope @event) { - if (@event.Payload is not AppEvent) + if (@event.Payload is AssetEvent assetEvent) { - return Task.CompletedTask; + var wrapper = new EventMessageWrapper(@event.To(), subscriptionCreators); + + return subscriptionService.PublishAsync($"asset-{assetEvent.AppId.Id}", wrapper); } - var wrapper = new EventMessageWrapper(@event.To(), subscriptionEventCreators); + if (@event.Payload is ContentEvent contentEvent) + { + var wrapper = new EventMessageWrapper(@event.To(), subscriptionCreators); + + return subscriptionService.PublishAsync($"content-{contentEvent.AppId.Id}", wrapper); + } - return subscriptionService.PublishAsync(wrapper); + return Task.CompletedTask; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index 961367e41..99c735e03 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCacheOptions.cs similarity index 50% rename from backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/AppCacheOptions.cs index 838094e17..3a2cc07c8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppCacheOptions.cs @@ -5,17 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Messaging.Subscriptions; +namespace Squidex.Domain.Apps.Entities.Apps; -namespace Squidex.Domain.Apps.Core.Subscriptions; - -public abstract class AppSubscription : ISubscription +public sealed class AppCacheOptions { - public DomainId AppId { get; set; } - - public PermissionSet Permissions { get; set; } - - public abstract ValueTask ShouldHandle(object message); + public TimeSpan CacheDuration { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs index 496ccd2e9..0aa18acbc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs @@ -35,9 +35,9 @@ public sealed class AppPermanentDeleter : IEventConsumer ]; } - public bool Handles(StoredEvent @event) + public ValueTask HandlesAsync(StoredEvent @event) { - return consumingTypes.Contains(@event.Data.Type); + return new ValueTask(consumingTypes.Contains(@event.Data.Type)); } public async Task On(Envelope @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs index d22e1be54..a0074ef0d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.Extensions.Options; using Squidex.Caching; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; @@ -22,16 +23,18 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes; public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); private readonly IAppRepository appRepository; private readonly IReplicatedCache appCache; + private readonly AppCacheOptions options; private readonly NameReservationState namesState; public AppsIndex(IAppRepository appRepository, IReplicatedCache appCache, - IPersistenceFactory persistenceFactory) + IPersistenceFactory persistenceFactory, + IOptions options) { this.appRepository = appRepository; this.appCache = appCache; + this.options = options.Value; namesState = new NameReservationState(persistenceFactory, "Apps"); } @@ -180,12 +183,8 @@ public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable private async Task CheckAppAsync(CreateApp command, CancellationToken ct) { - var token = await ReserveAsync(command.AppId, command.Name, ct); - - if (token == null) - { - throw new ValidationException(T.Get("apps.nameAlreadyExists")); - } + var token = await ReserveAsync(command.AppId, command.Name, ct) + ?? throw new ValidationException(T.Get("apps.nameAlreadyExists")); return token; } @@ -222,18 +221,28 @@ public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable private async Task PrepareAsync(App app) { + if (options.CacheDuration <= TimeSpan.Zero) + { + return app; + } + // Do not use cancellation here as we already so far. await appCache.AddAsync(new[] { new KeyValuePair(GetCacheKey(app.Id), app), new KeyValuePair(GetCacheKey(app.Name), app), - }, CacheDuration); + }, options.CacheDuration); return app; } private Task InvalidateItAsync(DomainId id, string name) { + if (options.CacheDuration <= TimeSpan.Zero) + { + return Task.CompletedTask; + } + // Do not use cancellation here as we already so far. return appCache.RemoveAsync(new[] { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs index 6e58fa73b..1c75db846 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs @@ -30,9 +30,9 @@ public sealed class AssetPermanentDeleter : IEventConsumer ]; } - public bool Handles(StoredEvent @event) + public ValueTask HandlesAsync(StoredEvent @event) { - return consumingTypes.Contains(@event.Data.Type); + return new ValueTask(consumingTypes.Contains(@event.Data.Type)); } public async Task On(Envelope @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index 802134333..4731ef86d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -45,9 +45,9 @@ public sealed class RecursiveDeleter : IEventConsumer ]; } - public bool Handles(StoredEvent @event) + public ValueTask HandlesAsync(StoredEvent @event) { - return consumingTypes.Contains(@event.Data.Type); + return new ValueTask(consumingTypes.Contains(@event.Data.Type)); } public async Task On(Envelope @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs index 497e00562..96298ab5c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs @@ -286,7 +286,7 @@ public sealed class RestoreJob : IJobRunner await eventStore.AppendUnsafeAsync(commits, ct); // Just in case we use parallel inserts later. - Interlocked.Increment(ref handled); + Interlocked.Add(ref handled, batch.Count); await run.LogAsync($"Reading {state.Reader.ReadEvents}/{handled} events and {state.Reader.ReadAttachments} attachments completed.", true); }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs index 7779117bd..a5cfe3527 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Reactive.Linq; using GraphQL; using GraphQL.Resolvers; using GraphQL.Types; @@ -13,6 +14,8 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; +using Squidex.Infrastructure.Translations; +using Squidex.Messaging.Subscriptions; using Squidex.Shared; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; @@ -129,13 +132,29 @@ internal static class AssetActions }, ]; - public static readonly ISourceStreamResolver Resolver = Resolvers.Stream(PermissionIds.AppAssetsRead, c => + public static readonly ISourceStreamResolver Resolver = new SourceStreamResolver(async fieldContext => { - return new AssetSubscription + var context = (GraphQLExecutionContext)fieldContext.UserContext; + + var app = context.Context.App; + + if (!context.Context.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.AppAssetsRead, app.Name))) + { + throw new DomainForbiddenException(T.Get("common.errorNoPermission")); + } + + var key = $"asset-{app.Id}"; + + var subscription = new AssetSubscription { - // Primary filter for the event types. - Type = c.GetArgument("type") + Type = fieldContext.GetArgument("type") }; + + var observable = + await context.Resolve() + .SubscribeAsync(key, subscription, fieldContext.CancellationToken); + + return observable.OfType(); }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index b854af08a..d63bd7a5a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Reactive.Linq; using GraphQL; using GraphQL.Resolvers; using GraphQL.Types; @@ -15,6 +16,8 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; +using Squidex.Infrastructure.Translations; +using Squidex.Messaging.Subscriptions; using Squidex.Shared; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; @@ -550,16 +553,35 @@ internal static class ContentActions }, ]; - public static readonly ISourceStreamResolver Resolver = Resolvers.Stream(PermissionIds.AppContentsRead, c => + public static readonly ISourceStreamResolver Resolver = new SourceStreamResolver(async fieldContext => { - return new ContentSubscription + var context = (GraphQLExecutionContext)fieldContext.UserContext; + + var app = context.Context.App; + + if (!context.Context.UserPermissions.Includes(PermissionIds.ForApp(PermissionIds.AppContentsRead, app.Name))) { + throw new DomainForbiddenException(T.Get("common.errorNoPermission")); + } + + var key = $"content-{app.Id}"; + + var subscription = new ContentSubscription + { + Permissions = context.Context.UserPermissions, + // Primary filter for the event types. - Type = c.GetArgument("type"), + Type = fieldContext.GetArgument("type"), // The name of the schema is used instead of the ID for a simpler API. - SchemaName = c.GetArgument("schemaName") + SchemaName = fieldContext.GetArgument("schemaName"), }; + + var observable = + await context.Resolve() + .SubscribeAsync(key, subscription, fieldContext.CancellationToken); + + return observable.OfType(); }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs index bda299005..b9a5b9e9d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs @@ -8,13 +8,10 @@ using GraphQL; using GraphQL.Execution; using GraphQL.Resolvers; -using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Translations; -using Squidex.Messaging.Subscriptions; -using Squidex.Shared; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; @@ -88,27 +85,4 @@ public static class Resolvers return commandContext.PlainResult!; }); } - - public static ISourceStreamResolver Stream(string permissionId, Func action) - { - return new SourceStreamResolver(fieldContext => - { - var context = (GraphQLExecutionContext)fieldContext.UserContext; - - if (!context.Context.UserPermissions.Includes(PermissionIds.ForApp(permissionId, context.Context.App.Name))) - { - throw new DomainForbiddenException(T.Get("common.errorNoPermission")); - } - - var subscription = action(fieldContext); - - // The app id is taken from the URL so we cannot get events from other apps. - subscription.AppId = context.Context.App.Id; - - // We also check the subscriptions on the source server. - subscription.Permissions = context.Context.UserPermissions; - - return context.Resolve().Subscribe(subscription); - }); - } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs index c552f2dc4..41c154ad8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs @@ -6,7 +6,6 @@ // ========================================================================== using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure; using Squidex.Messaging; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs index acdc34fef..376209987 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.Extensions.Options; using Squidex.Caching; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; @@ -19,17 +20,19 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes; public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); private readonly ISchemaRepository schemaRepository; private readonly IReplicatedCache schemaCache; private readonly IPersistenceFactory persistenceFactory; + private readonly SchemaCacheOptions options; public SchemasIndex(ISchemaRepository schemaRepository, IReplicatedCache schemaCache, - IPersistenceFactory persistenceFactory) + IPersistenceFactory persistenceFactory, + IOptions options) { this.schemaRepository = schemaRepository; this.schemaCache = schemaCache; this.persistenceFactory = persistenceFactory; + this.options = options.Value; } public async Task> GetSchemasAsync(DomainId appId, @@ -209,18 +212,28 @@ public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex // Run some fallback migrations. schema = FieldNames.Migrate(schema); + if (options.CacheDuration <= TimeSpan.Zero) + { + return schema; + } + // Do not use cancellation here as we already so far. await schemaCache.AddAsync(new[] { new KeyValuePair(GetCacheKey(schema.AppId.Id, schema.Id), schema), new KeyValuePair(GetCacheKey(schema.AppId.Id, schema.Name), schema), - }, CacheDuration); + }, options.CacheDuration); return schema; } private Task InvalidateItAsync(DomainId appId, DomainId id, string name) { + if (options.CacheDuration <= TimeSpan.Zero) + { + return Task.CompletedTask; + } + // Do not use cancellation here as we already so far. return schemaCache.RemoveAsync(new[] { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCacheOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCacheOptions.cs new file mode 100644 index 000000000..3cf6bd6d3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCacheOptions.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Schemas; + +public sealed class SchemaCacheOptions +{ + public TimeSpan CacheDuration { get; set; } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 15f54fa59..fcef55599 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -27,7 +27,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index d484250e7..81899ded3 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index dcdfa3328..7d5dbd4e2 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 8e355d2fc..05455182f 100644 --- a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -18,13 +18,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj index c808539d7..2ac186a70 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -11,11 +11,11 @@ True - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 29ac5948d..09f675aaf 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs index 2f10f7052..dc2f8df94 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.Commands; public sealed class DefaultDomainObjectCache : IDomainObjectCache { - private readonly DistributedCacheEntryOptions cacheOptions; + private readonly DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions(); private readonly IMemoryCache cache; private readonly IJsonSerializer serializer; private readonly IDistributedCache distributedCache; @@ -27,15 +27,20 @@ public sealed class DefaultDomainObjectCache : IDomainObjectCache this.serializer = serializer; this.distributedCache = distributedCache; - cacheOptions = new DistributedCacheEntryOptions + if (options.Value.CacheDuration > TimeSpan.Zero) { - AbsoluteExpirationRelativeToNow = options.Value.CacheDuration - }; + cacheOptions.AbsoluteExpirationRelativeToNow = options.Value.CacheDuration; + } } public async Task GetAsync(DomainId id, long version, CancellationToken ct = default) { + if (cacheOptions.AbsoluteExpirationRelativeToNow == null) + { + return default!; + } + var cacheKey = CacheKey(id, version); if (cache.TryGetValue(cacheKey, out var found) && found is T typed) @@ -68,10 +73,14 @@ public sealed class DefaultDomainObjectCache : IDomainObjectCache public async Task SetAsync(DomainId id, long version, T snapshot, CancellationToken ct = default) { + if (cacheOptions.AbsoluteExpirationRelativeToNow == null) + { + return; + } + var cacheKey = CacheKey(id, version); cache.Set(cacheKey, snapshot, cacheOptions.AbsoluteExpirationRelativeToNow!.Value); - try { using (var stream = DefaultPools.MemoryStream.GetStream()) diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs index a75380ec7..64c33bcf1 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs @@ -53,7 +53,7 @@ internal sealed class ParseSubscription : IEventSubscriber, IEventS { Envelope? @event = null; - if (eventConsumer.Handles(storedEvent)) + if (await eventConsumer.HandlesAsync(storedEvent)) { @event = eventFormatter.ParseIfKnown(storedEvent); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs index 1296fcecf..80b097429 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs @@ -7,37 +7,19 @@ namespace Squidex.Infrastructure.EventSourcing; -public class Envelope where T : class, IEvent +public sealed class Envelope(T payload, EnvelopeHeaders? headers = null) where T : class, IEvent { - private readonly EnvelopeHeaders headers; - private readonly T payload; + public EnvelopeHeaders Headers { get; } = headers ?? []; - public EnvelopeHeaders Headers - { - get => headers; - } - - public T Payload - { - get => payload; - } - - public Envelope(T payload, EnvelopeHeaders? headers = null) - { - Guard.NotNull(payload); - - this.payload = payload; - - this.headers = headers ?? []; - } + public T Payload { get; } = Guard.NotNull(payload); public Envelope To() where TOther : class, IEvent { - return new Envelope((payload as TOther)!, headers.CloneHeaders()); + return new Envelope((payload as TOther)!, Headers.CloneHeaders()); } public static implicit operator Envelope(Envelope source) { - return source == null ? source! : new Envelope(source.payload, source.headers); + return source == null ? source! : new Envelope(source.Payload, source.Headers); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs index 78331f763..f3a412627 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs @@ -21,9 +21,9 @@ public interface IEventConsumer bool CanClear => true; - bool Handles(StoredEvent @event) + ValueTask HandlesAsync(StoredEvent @event) { - return true; + return new ValueTask(true); } Task ClearAsync() diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 3d5701f1f..cbba05e56 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -13,23 +13,23 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - - - - + + + + + + diff --git a/backend/src/Squidex.Shared/Squidex.Shared.csproj b/backend/src/Squidex.Shared/Squidex.Shared.csproj index 53a791d99..a07d24137 100644 --- a/backend/src/Squidex.Shared/Squidex.Shared.csproj +++ b/backend/src/Squidex.Shared/Squidex.Shared.csproj @@ -10,7 +10,7 @@ True - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs b/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs index dc3bb5dc7..a8069a8ff 100644 --- a/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs @@ -37,7 +37,6 @@ public sealed class SetupMiddleware else { isUserFound = true; - await next(context); } } diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index bb7daafc4..cb1bd7205 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs index 695941d98..10bc25ff0 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -6,8 +6,6 @@ // ========================================================================== using System.Text.Json; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using NJsonSchema; using NJsonSchema.Generation; using NJsonSchema.Generation.TypeMappers; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs index dc3a510d7..ca2cd7df1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs @@ -13,6 +13,11 @@ namespace Squidex.Areas.Api.Controllers.Translations.Models; [OpenApiRequest] public sealed class AskDto { + /// + /// Optional conversation ID. + /// + public string? ConversationId { get; set; } + /// /// The text to ask. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index bc6bc23a8..979f96b12 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -6,10 +6,10 @@ // ========================================================================== using Microsoft.AspNetCore.Mvc; +using Squidex.AI; using Squidex.Areas.Api.Controllers.Translations.Models; using Squidex.Infrastructure.Commands; using Squidex.Shared; -using Squidex.Text.ChatBots; using Squidex.Text.Translations; using Squidex.Web; @@ -64,17 +64,9 @@ public sealed class TranslationsController : ApiController [ApiCosts(10)] public async Task PostQuestion(string app, [FromBody] AskDto request) { - var conversationId = Guid.NewGuid().ToString(); - try - { - var result = await chatAgent.PromptAsync(conversationId, request.Prompt, HttpContext.RequestAborted); - var response = new string[] { result.Text }; + var result = await chatAgent.PromptAsync(request.Prompt, request.ConversationId, HttpContext.RequestAborted); + var response = new string[] { result.Text }; - return Ok(response); - } - finally - { - await chatAgent.StopConversationAsync(conversationId, default); - } + return Ok(response); } } diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index 8b92ebf66..95ec7d6a9 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Invitation; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.DomainObject; using Squidex.Domain.Apps.Entities.Schemas.Indexes; @@ -39,9 +40,17 @@ public static class CommandsServices services.Configure(config, "usage"); + services.Configure(config, + "caching:apps"); + services.Configure(config, "caching:domainObjects"); + services.Configure(config, + "caching:schemas"); + + services.ConfigureForObsoleteReplicatedCacheSetting(config); + services.AddSingletonAs() .As(); @@ -135,4 +144,25 @@ public static class CommandsServices services.AddSingletonAs() .As(); } + + private static void ConfigureForObsoleteReplicatedCacheSetting(this IServiceCollection services, IConfiguration config) + { + var isCaching = config.GetValue("caching:replicated:enable"); + + services.Configure(options => + { + if (options.CacheDuration == default && isCaching) + { + options.CacheDuration = TimeSpan.FromMinutes(5); + } + }); + + services.Configure(options => + { + if (options.CacheDuration == default && isCaching) + { + options.CacheDuration = TimeSpan.FromMinutes(5); + } + }); + } } diff --git a/backend/src/Squidex/Config/Domain/FontendServices.cs b/backend/src/Squidex/Config/Domain/FontendServices.cs index 02085dd09..c0a296d31 100644 --- a/backend/src/Squidex/Config/Domain/FontendServices.cs +++ b/backend/src/Squidex/Config/Domain/FontendServices.cs @@ -7,10 +7,10 @@ using System.Text.Json; using Microsoft.Extensions.Options; +using Squidex.AI; using Squidex.Areas.Api.Controllers.UI; using Squidex.Domain.Apps.Entities.History; using Squidex.Hosting; -using Squidex.Text.ChatBots; using Squidex.Text.Translations; using Squidex.Web; diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index d9d33e812..72593d7c9 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -6,12 +6,12 @@ // ========================================================================== using Microsoft.Extensions.Caching.Memory; +using Microsoft.SemanticKernel; using NodaTime; using Squidex.Areas.Api.Controllers.Contents.Generator; using Squidex.Areas.Api.Controllers.News; using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Areas.Api.Controllers.UI; -using Squidex.Caching; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Domain.Apps.Core.Tags; @@ -38,9 +38,6 @@ public static class InfrastructureServices services.Configure(config, "exposedConfiguration"); - services.Configure(config, - "caching:replicated"); - services.Configure(config, "scripting"); @@ -133,6 +130,16 @@ public static class InfrastructureServices services.AddSingletonAs() .AsSelf(); + var kernel = services.AddKernel(); + + var openAiKey = config["chatBot:openAi:apiKey"]; + var openAiModel = config["chatBot:openAi:model"] ?? "gpt-3.5-turbo-0125"; + + if (!string.IsNullOrWhiteSpace(openAiKey)) + { + kernel.AddOpenAIChatCompletion(openAiModel, openAiKey); + } + services.AddDeepLTranslations(config); services.AddGoogleCloudTranslations(config); services.AddOpenAIChatAgent(config); diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index b1796cbb4..3ea946e3f 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -75,6 +75,15 @@ public static class StoreServices options.DatabaseName = mongoDatabaseName; }); + services.AddKernel() + .AddMongoChatStore(config, options => + { + options.CollectionName = "Chat"; + }); + + services.AddMessaging() + .AddMongoDataStore(config); + services.AddSingletonAs(c => GetMongoClient(mongoConfiguration)) .As(); diff --git a/backend/src/Squidex/Config/Messaging/MessagingServices.cs b/backend/src/Squidex/Config/Messaging/MessagingServices.cs index 34ad8e5ac..f0a9a7e59 100644 --- a/backend/src/Squidex/Config/Messaging/MessagingServices.cs +++ b/backend/src/Squidex/Config/Messaging/MessagingServices.cs @@ -22,7 +22,6 @@ using Squidex.Messaging; using Squidex.Messaging.Implementation; using Squidex.Messaging.Implementation.Null; using Squidex.Messaging.Implementation.Scheduler; -using Squidex.Messaging.Subscriptions; namespace Squidex.Config.Messaging; @@ -30,11 +29,14 @@ public static class MessagingServices { public static void AddSquidexMessaging(this IServiceCollection services, IConfiguration config) { + services.Configure(config, + "messaging"); + var channelBackupRestore = new ChannelName("backup.restore"); var channelBackupStart = new ChannelName("backup.start"); var channelFallback = new ChannelName("default"); var channelRules = new ChannelName("rules.run"); - var isCaching = config.GetValue("caching:replicated:enable"); + var isRandomName = config.GetValue("clustering:randomName"); var isWorker = config.GetValue("clustering:worker"); if (isWorker) @@ -61,6 +63,12 @@ public static class MessagingServices .AsSelf().As(); } + if (isRandomName) + { + services.AddSingletonAs() + .As(); + } + services.AddSingletonAs() .As(); @@ -76,55 +84,43 @@ public static class MessagingServices services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As().As(); - services.AddReplicatedCacheMessaging(isCaching, options => - { - options.TransportSelector = (transport, _) => transport.First(x => x is NullTransport != isCaching); - }); - - services.Configure(options => - { - options.SendMessagesToSelf = false; - }); - - services.AddMessagingSubscriptions(); - services.AddMessagingTransport(config); - services.AddMessaging(options => - { - options.Routing.Add(m => m is JobStart r && r.Request.TaskName == BackupJob.TaskName, channelBackupStart); - options.Routing.Add(m => m is JobStart r && r.Request.TaskName == RestoreJob.TaskName, channelBackupRestore); - options.Routing.Add(m => m is JobStart r && r.Request.TaskName == RuleRunnerJob.TaskName, channelRules); - options.Routing.AddFallback(channelFallback); - }); - - services.AddMessaging(channelBackupStart, isWorker, options => - { - options.Timeout = TimeSpan.FromHours(4); - options.Scheduler = new ParallelScheduler(4); - options.LogMessage = x => true; - }); - - services.AddMessaging(channelBackupRestore, isWorker, options => - { - options.Timeout = TimeSpan.FromHours(24); - options.Scheduler = InlineScheduler.Instance; - options.LogMessage = x => true; - }); - - services.AddMessaging(channelRules, isWorker, options => - { - options.Scheduler = new ParallelScheduler(4); - options.LogMessage = x => true; - }); - - services.AddMessaging(channelFallback, isWorker, options => - { - options.Scheduler = InlineScheduler.Instance; - }); + services.AddMessaging() + .AddTransport(config) + .AddSubscriptions(!isWorker) + .AddReplicatedCache(true, options => + { + options.TransportSelector = (transport, _) => transport.First(x => x is not NullTransport); + }) + .Configure(options => + { + options.Routing.Add(m => m is JobStart r && r.Request.TaskName == BackupJob.TaskName, channelBackupStart); + options.Routing.Add(m => m is JobStart r && r.Request.TaskName == RestoreJob.TaskName, channelBackupRestore); + options.Routing.Add(m => m is JobStart r && r.Request.TaskName == RuleRunnerJob.TaskName, channelRules); + options.Routing.AddFallback(channelFallback); + }) + .AddChannel(channelBackupStart, isWorker, options => + { + options.Timeout = TimeSpan.FromHours(4); + options.Scheduler = new ParallelScheduler(4); + options.LogMessage = x => true; + }) + .AddChannel(channelBackupRestore, isWorker, options => + { + options.Timeout = TimeSpan.FromHours(24); + options.Scheduler = InlineScheduler.Instance; + options.LogMessage = x => true; + }) + .AddChannel(channelRules, isWorker, options => + { + options.Scheduler = new ParallelScheduler(4); + options.LogMessage = x => true; + }) + .AddChannel(channelFallback, isWorker, options => + { + options.Scheduler = InlineScheduler.Instance; + }); } } diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 187984883..a2efa1645 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -38,44 +38,44 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + - + - - + + - - - - - - - - - + + + + + + + + + - - - + + + diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 21124e74f..2f9db6842 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -73,10 +73,26 @@ "maxSurrogateKeysSize": 0, "replicated": { + // OBSOLETE // Set to true to enable a replicated cache for app, schemas and rules. Increases performance but reduces consistency. + // + // This setting is obsolete and has been replaced with + // * caching:apps:cacheDuration + // * caching:schemas:cacheDuration + // "enable": true }, + "apps": { + // The cache duration for apps. + "cacheDuration": "00:00:00" + }, + + "schemas": { + // The cache duration for schemas. + "cacheDuration": "00:00:00" + }, + "domainObjects": { // The cache duration for domain objects. "cacheDuration": "00:10:00" @@ -663,7 +679,10 @@ "chatbot": { "openai": { // The OpenAI API Key. - "apiKey": "" + "apiKey": "", + + // The chat model. + "model": "gpt-3.5-turbo-0125" } }, diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs index 43b445a08..f38551149 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -9,13 +9,13 @@ using System.Net; using System.Text; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using Squidex.AI; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; -using Squidex.Text.ChatBots; using Squidex.Text.Translations; namespace Squidex.Domain.Apps.Core.Operations.Scripting; @@ -619,7 +619,7 @@ public class JintScriptEngineHelperTests : IClassFixture [Fact] public async Task Should_generate_content() { - A.CallTo(() => chatAgent.PromptAsync(A._, "prompt", A._)) + A.CallTo(() => chatAgent.PromptAsync("prompt", A._, A._)) .Returns(ChatBotResponse.Success("Generated")); var vars = new ScriptVars @@ -637,7 +637,7 @@ public class JintScriptEngineHelperTests : IClassFixture Assert.Equal("Generated", actual.ToString()); A.CallTo(() => chatAgent.StopConversationAsync(A._, A._)) - .MustHaveHappened(); + .MustNotHaveHappened(); } [Theory] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs index 7ac680a95..35598328f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs @@ -10,8 +10,8 @@ using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; + +#pragma warning disable CA1859 // Use concrete types when possible for improved performance namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; @@ -22,7 +22,7 @@ public class AssetSubscriptionTests [Fact] public async Task Should_return_true_for_enriched_asset_event() { - var sut = WithPermission(new AssetSubscription()); + var sut = new AssetSubscription(); var @event = Enrich(new EnrichedAssetEvent()); @@ -32,7 +32,7 @@ public class AssetSubscriptionTests [Fact] public async Task Should_return_false_for_wrong_event() { - var sut = WithPermission(new AssetSubscription()); + var sut = new AssetSubscription(); var @event = new AppCreated(); @@ -42,7 +42,7 @@ public class AssetSubscriptionTests [Fact] public async Task Should_return_true_for_asset_event() { - var sut = WithPermission(new AssetSubscription()); + var sut = new AssetSubscription(); var @event = Enrich(new AssetCreated()); @@ -52,7 +52,7 @@ public class AssetSubscriptionTests [Fact] public async Task Should_return_true_for_asset_event_with_correct_type() { - var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Created }); + var sut = new AssetSubscription { Type = EnrichedAssetEventType.Created }; var @event = Enrich(new AssetCreated()); @@ -62,17 +62,7 @@ public class AssetSubscriptionTests [Fact] public async Task Should_return_false_for_asset_event_with_wrong_type() { - var sut = WithPermission(new AssetSubscription { Type = EnrichedAssetEventType.Deleted }); - - var @event = Enrich(new AssetCreated()); - - Assert.False(await sut.ShouldHandle(@event)); - } - - [Fact] - public async Task Should_return_false_for_asset_event_invalid_permissions() - { - var sut = WithPermission(new AssetSubscription(), PermissionIds.AppCommentsCreate); + var sut = new AssetSubscription { Type = EnrichedAssetEventType.Deleted }; var @event = Enrich(new AssetCreated()); @@ -82,28 +72,12 @@ public class AssetSubscriptionTests private object Enrich(EnrichedAssetEvent source) { source.AppId = appId; - return source; } private object Enrich(AssetEvent source) { source.AppId = appId; - return source; } - - private AssetSubscription WithPermission(AssetSubscription subscription, string? permissionId = null) - { - subscription.AppId = appId.Id; - - permissionId ??= PermissionIds.AppAssetsRead; - - var permission = PermissionIds.ForApp(permissionId, appId.Name); - var permissions = new PermissionSet(permission); - - subscription.Permissions = permissions; - - return subscription; - } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs index 800f102ae..a0a4184ba 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs @@ -13,6 +13,8 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; using Squidex.Shared; +#pragma warning disable CA1859 // Use concrete types when possible for improved performance + namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; public class ContentSubscriptionTests @@ -104,7 +106,6 @@ public class ContentSubscriptionTests { source.AppId = appId; source.SchemaId = schemaId; - return source; } @@ -112,14 +113,11 @@ public class ContentSubscriptionTests { source.AppId = appId; source.SchemaId = schemaId; - return source; } private ContentSubscription WithPermission(ContentSubscription subscription, string? permissionId = null) { - subscription.AppId = appId.Id; - permissionId ??= PermissionIds.AppContentsRead; var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs deleted file mode 100644 index 9ce2eb944..000000000 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Subscriptions; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; - -public class EventMessageEvaluatorTests -{ - private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - private readonly EventMessageEvaluator sut = new EventMessageEvaluator(); - - [Fact] - public async Task Should_return_empty_list_when_nothing_registered() - { - var assetEvent = new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") }; - - var subscriptions = await sut.GetSubscriptionsAsync(assetEvent); - - Assert.Empty(subscriptions); - } - - [Fact] - public async Task Should_return_matching_subscriptions() - { - var contentSubscriptionId = Guid.NewGuid(); - var contentSubscription = WithPermission(new ContentSubscription(), PermissionIds.AppContentsRead); - - var assetSubscriptionId = Guid.NewGuid(); - var assetSubscription = WithPermission(new AssetSubscription(), PermissionIds.AppAssetsRead); - - sut.SubscriptionAdded(contentSubscriptionId, contentSubscription); - sut.SubscriptionAdded(assetSubscriptionId, assetSubscription); - - Assert.Equal(new[] { contentSubscriptionId }, - await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); - - Assert.Equal(new[] { assetSubscriptionId }, - await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); - - Assert.Empty( - await sut.GetSubscriptionsAsync(Enrich(new AppCreated()))); - - Assert.Empty( - await sut.GetSubscriptionsAsync(new ContentCreated { AppId = NamedId.Of(DomainId.NewGuid(), "my-app2") })); - - sut.SubscriptionRemoved(contentSubscriptionId, contentSubscription); - sut.SubscriptionRemoved(assetSubscriptionId, assetSubscription); - - Assert.Empty( - await sut.GetSubscriptionsAsync(Enrich(new ContentCreated()))); - - Assert.Empty( - await sut.GetSubscriptionsAsync(Enrich(new AssetCreated()))); - } - - private object Enrich(ContentEvent source) - { - source.SchemaId = schemaId; - source.AppId = appId; - - return source; - } - - private object Enrich(AppEvent source) - { - source.Actor = null!; - source.AppId = appId; - - return source; - } - - private AppSubscription WithPermission(AppSubscription subscription, string permissionId) - { - subscription.AppId = appId.Id; - - var permission = PermissionIds.ForApp(permissionId, appId.Name, schemaId.Name); - var permissions = new PermissionSet(permission); - - subscription.Permissions = permissions; - - return subscription; - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs index 7594fe0cc..bc4cb589d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs @@ -6,7 +6,9 @@ // ========================================================================== using Squidex.Domain.Apps.Core.Subscriptions; -using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Messaging.Subscriptions; @@ -14,6 +16,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Subscriptions; public class SubscriptionPublisherTests { + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly ISubscriptionService subscriptionService = A.Fake(); private readonly SubscriptionPublisher sut; @@ -59,14 +62,16 @@ public class SubscriptionPublisherTests [Theory] [InlineData(true)] [InlineData(false)] - public void Should_handle_events_when_subscription_exists(bool hasSubscriptions) + public async Task Should_handle_events_when_subscription_exists(bool hasSubscriptions) { - A.CallTo(() => subscriptionService.HasSubscriptions) - .Returns(hasSubscriptions); + var storedEvent = + new StoredEvent($"asset-{DomainId.Combine(appId, DomainId.NewGuid())}", $"0", 0, + new EventData("Type", [], "Payload")); - IEventConsumer consumer = sut; + A.CallTo(() => subscriptionService.HasSubscriptionsAsync($"asset-{appId.Id}", default)) + .Returns(hasSubscriptions); - Assert.Equal(hasSubscriptions, consumer.Handles(null!)); + Assert.Equal(hasSubscriptions, await sut.HandlesAsync(storedEvent)); } [Fact] @@ -81,13 +86,28 @@ public class SubscriptionPublisherTests } [Fact] - public async Task Should_publish_app_event() + public async Task Should_publish_asset_event() + { + var envelope = + Envelope.Create( + new AssetCreated { AppId = appId, AssetId = DomainId.NewGuid() }); + + await sut.On(envelope); + + A.CallTo(() => subscriptionService.PublishAsync($"asset-{appId.Id}", A._, default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_publish_content_event() { - var envelope = Envelope.Create(new AppCreated()); + var envelope = + Envelope.Create( + new ContentCreated { AppId = appId, ContentId = DomainId.NewGuid() }); await sut.On(envelope); - A.CallTo(subscriptionService).Where(x => x.Method.Name.StartsWith("Publish")) + A.CallTo(() => subscriptionService.PublishAsync($"content-{appId.Id}", A._, default)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 8a6c45d14..6d0fdb682 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,7 +24,7 @@ - + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs index 3a43cc26e..61cc18106 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs @@ -9,7 +9,6 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities; @@ -18,15 +17,6 @@ public class AppProviderExtensionsTests : GivenContext private readonly NamedId componentId1 = NamedId.Of(DomainId.NewGuid(), "my-schema"); private readonly NamedId componentId2 = NamedId.Of(DomainId.NewGuid(), "my-schema"); - [Fact] - public void X() - { - var y = DomainId.Create("c3750ec4-baf1-44af-85f0-1495ab4f9f1a"); - - var x = new PartitionedSharding(20).GetShardKey(y); - } - - [Fact] public async Task Should_do_nothing_if_no_component_found() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs index 0e03a33f4..d83f9b1b7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs @@ -45,7 +45,7 @@ public class AppPermanentDeleterTests : GivenContext } [Fact] - public void Should_handle_delete_event() + public async Task Should_handle_delete_event() { var eventType = TestUtils.TypeRegistry.GetName(); @@ -53,11 +53,11 @@ public class AppPermanentDeleterTests : GivenContext new StoredEvent("stream", "1", 1, new EventData(eventType, [], "payload")); - Assert.True(sut.Handles(storedEvent)); + Assert.True(await sut.HandlesAsync(storedEvent)); } [Fact] - public void Should_handle_contributor_event() + public async Task Should_handle_contributor_event() { var eventType = TestUtils.TypeRegistry.GetName(); @@ -65,11 +65,11 @@ public class AppPermanentDeleterTests : GivenContext new StoredEvent("stream", "1", 1, new EventData(eventType, [], "payload")); - Assert.True(sut.Handles(storedEvent)); + Assert.True(await sut.HandlesAsync(storedEvent)); } [Fact] - public void Should_not_handle_creation_event() + public async Task Should_not_handle_creation_event() { var eventType = TestUtils.TypeRegistry.GetName(); @@ -77,7 +77,7 @@ public class AppPermanentDeleterTests : GivenContext new StoredEvent("stream", "1", 1, new EventData(eventType, [], "payload")); - Assert.False(sut.Handles(storedEvent)); + Assert.False(await sut.HandlesAsync(storedEvent)); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs index 64eaccb57..387717406 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -28,17 +28,19 @@ public class AppsIndexTests : GivenContext private readonly TestState state; private readonly IAppRepository appRepository = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); + private readonly AppCacheOptions options = new AppCacheOptions(); private readonly AppsIndex sut; public AppsIndexTests() { + options.CacheDuration = TimeSpan.FromMinutes(5); + state = new TestState("Apps"); var replicatedCache = - new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake(), - Options.Create(new ReplicatedCacheOptions { Enable = true })); + new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake()); - sut = new AppsIndex(appRepository, replicatedCache, state.PersistenceFactory); + sut = new AppsIndex(appRepository, replicatedCache, state.PersistenceFactory, Options.Create(options)); } [Fact] @@ -61,20 +63,38 @@ public class AppsIndexTests : GivenContext public async Task Should_resolve_app_by_name_and_id_if_cached_before() { A.CallTo(() => appRepository.FindAsync(AppId.Name, CancellationToken)) - .Returns(App); + .ReturnsLazily(() => App with { Version = 3 }); var actual1 = await sut.GetAppAsync(AppId.Name, true, CancellationToken); var actual2 = await sut.GetAppAsync(AppId.Name, true, CancellationToken); var actual3 = await sut.GetAppAsync(AppId.Id, true, CancellationToken); - Assert.Same(App, actual1); - Assert.Same(App, actual2); - Assert.Same(App, actual3); + Assert.Same(actual1, actual2); + Assert.Same(actual1, actual3); A.CallTo(() => appRepository.FindAsync(AppId.Name, CancellationToken)) .MustHaveHappenedOnceExactly(); } + [Fact] + public async Task Should_not_resolve_app_by_name_and_id_if_cache_before_but_disabled() + { + options.CacheDuration = default; + + A.CallTo(() => appRepository.FindAsync(AppId.Name, CancellationToken)) + .ReturnsLazily(() => App with { Version = 3 }); + + var actual1 = await sut.GetAppAsync(AppId.Name, true, CancellationToken); + var actual2 = await sut.GetAppAsync(AppId.Name, true, CancellationToken); + var actual3 = await sut.GetAppAsync(AppId.Id, true, CancellationToken); + + Assert.NotSame(actual1, actual2); + Assert.NotSame(actual1, actual3); + + A.CallTo(() => appRepository.FindAsync(AppId.Name, CancellationToken)) + .MustHaveHappenedTwiceExactly(); + } + [Fact] public async Task Should_resolve_app_by_id() { @@ -84,8 +104,8 @@ public class AppsIndexTests : GivenContext var actual1 = await sut.GetAppAsync(AppId.Id, false, CancellationToken); var actual2 = await sut.GetAppAsync(AppId.Id, false, CancellationToken); - Assert.Same(App, actual1); - Assert.Same(App, actual2); + Assert.Equal(App, actual1); + Assert.Equal(App, actual2); A.CallTo(() => appRepository.FindAsync(AppId.Id, CancellationToken)) .MustHaveHappenedTwiceExactly(); @@ -94,21 +114,41 @@ public class AppsIndexTests : GivenContext [Fact] public async Task Should_resolve_app_by_id_and_name_if_cached_before() { + options.CacheDuration = TimeSpan.FromMinutes(5); + A.CallTo(() => appRepository.FindAsync(AppId.Id, CancellationToken)) - .Returns(App); + .ReturnsLazily(() => App with { Version = 3 }); var actual1 = await sut.GetAppAsync(AppId.Id, true, CancellationToken); var actual2 = await sut.GetAppAsync(AppId.Id, true, CancellationToken); var actual3 = await sut.GetAppAsync(AppId.Name, true, CancellationToken); - Assert.Same(App, actual1); - Assert.Same(App, actual2); - Assert.Same(App, actual3); + Assert.Same(actual1, actual2); + Assert.Same(actual1, actual3); A.CallTo(() => appRepository.FindAsync(AppId.Id, CancellationToken)) .MustHaveHappenedOnceExactly(); } + [Fact] + public async Task Should_not_resolve_app_by_id_and_name_if_cached_before_but_disabled() + { + options.CacheDuration = default; + + A.CallTo(() => appRepository.FindAsync(AppId.Id, CancellationToken)) + .ReturnsLazily(() => App with { Version = 3 }); + + var actual1 = await sut.GetAppAsync(AppId.Id, true, CancellationToken); + var actual2 = await sut.GetAppAsync(AppId.Id, true, CancellationToken); + var actual3 = await sut.GetAppAsync(AppId.Name, true, CancellationToken); + + Assert.NotSame(actual1, actual2); + Assert.NotSame(actual1, actual3); + + A.CallTo(() => appRepository.FindAsync(AppId.Id, CancellationToken)) + .MustHaveHappenedTwiceExactly(); + } + [Fact] public async Task Should_resolve_all_apps_from_user_permissions() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 2078e8be7..170bfc278 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -18,7 +18,6 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure.EventSourcing; -using Xunit; namespace Squidex.Domain.Apps.Entities.Assets; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs index 7e53ac7e4..4ce6a19f9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs @@ -43,7 +43,7 @@ public class AssetPermanentDeleterTests : GivenContext } [Fact] - public void Should_handle_deletion_event() + public async Task Should_handle_deletion_event() { var eventType = TestUtils.TypeRegistry.GetName(); @@ -51,11 +51,11 @@ public class AssetPermanentDeleterTests : GivenContext new StoredEvent("stream", "1", 1, new EventData(eventType, [], "payload")); - Assert.True(sut.Handles(storedEvent)); + Assert.True(await sut.HandlesAsync(storedEvent)); } [Fact] - public void Should_not_handle_creation_event() + public async Task Should_not_handle_creation_event() { var eventType = TestUtils.TypeRegistry.GetName(); @@ -63,7 +63,7 @@ public class AssetPermanentDeleterTests : GivenContext new StoredEvent("stream", "1", 1, new EventData(eventType, [], "payload")); - Assert.False(sut.Handles(storedEvent)); + Assert.False(await sut.HandlesAsync(storedEvent)); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 5b83a489b..91ba6e42f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -9,7 +9,6 @@ using Squidex.Assets; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Assets; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs index baa7fec4e..ba9086c4d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs @@ -30,7 +30,7 @@ public class GraphQLSubscriptionTests : GraphQLTestBase FileSize = 1024 }); - A.CallTo(() => subscriptionService.Subscribe(A._)) + A.CallTo(() => subscriptionService.SubscribeAsync($"asset-{TestApp.Default.Id}", A._, default)) .Returns(stream); var actual = await ExecuteAsync(new TestQuery @@ -128,7 +128,7 @@ public class GraphQLSubscriptionTests : GraphQLTestBase .AddInvariant(42)) }); - A.CallTo(() => subscriptionService.Subscribe(A._)) + A.CallTo(() => subscriptionService.SubscribeAsync($"content-{TestApp.Default.Id}", A._, default)) .Returns(stream); var actual = await ExecuteAsync(new TestQuery diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index c7f9cd338..305e2b189 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -95,7 +95,7 @@ public abstract class GraphQLTestBase : IClassFixture var actual = await new DocumentExecuter().ExecuteAsync(options); - if (actual.Streams?.Count > 0 && actual.Errors?.Any() != true) + if (actual.Streams is { Count: > 0 } && actual.Errors is not { Count: > 0 }) { // Resolve the first stream actual with a timeout. var stream = actual.Streams.First(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs index 056fd86a5..d151c0c53 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Elasticsearch.Net.Specification.CrossClusterReplicationApi; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs index 76436edee..3fa964540 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs @@ -11,7 +11,6 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; #pragma warning disable SA1310 // Field names must not contain underscore diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs index 27027ad4a..426ec225e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -26,17 +26,19 @@ public class SchemasIndexTests : GivenContext private readonly TestState state; private readonly ISchemaRepository schemaRepository = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); + private readonly SchemaCacheOptions options = new SchemaCacheOptions(); private readonly SchemasIndex sut; public SchemasIndexTests() { + options.CacheDuration = TimeSpan.FromMinutes(5); + state = new TestState($"{AppId.Id}_Schemas"); var replicatedCache = - new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake(), - Options.Create(new ReplicatedCacheOptions { Enable = true })); + new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake()); - sut = new SchemasIndex(schemaRepository, replicatedCache, state.PersistenceFactory); + sut = new SchemasIndex(schemaRepository, replicatedCache, state.PersistenceFactory, Options.Create(options)); } [Fact] @@ -59,20 +61,38 @@ public class SchemasIndexTests : GivenContext public async Task Should_resolve_schema_by_name_and_id_if_cached_before() { A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Name, CancellationToken)) - .Returns(Schema); + .ReturnsLazily(() => Schema with { Version = 3 }); var actual1 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Name, true, CancellationToken); var actual2 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Name, true, CancellationToken); var actual3 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Id, true, CancellationToken); - Assert.Same(Schema, actual1); - Assert.Same(Schema, actual2); - Assert.Same(Schema, actual3); + Assert.Same(actual1, actual2); + Assert.Same(actual1, actual3); A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Name, CancellationToken)) .MustHaveHappenedOnceExactly(); } + [Fact] + public async Task Should_not_resolve_schema_by_name_and_id_if_cached_before_but_disabled() + { + options.CacheDuration = default; + + A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Name, CancellationToken)) + .ReturnsLazily(() => Schema with { Version = 3 }); + + var actual1 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Name, true, CancellationToken); + var actual2 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Name, true, CancellationToken); + var actual3 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Id, true, CancellationToken); + + Assert.NotSame(actual1, actual2); + Assert.NotSame(actual1, actual3); + + A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Name, CancellationToken)) + .MustHaveHappenedTwiceExactly(); + } + [Fact] public async Task Should_resolve_schema_by_id() { @@ -93,20 +113,38 @@ public class SchemasIndexTests : GivenContext public async Task Should_resolve_schema_by_id_and_name_if_cached_before() { A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Id, CancellationToken)) - .Returns(Schema); + .ReturnsLazily(() => Schema with { Version = 3 }); var actual1 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Id, true, CancellationToken); var actual2 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Id, true, CancellationToken); var actual3 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Name, true, CancellationToken); - Assert.Same(Schema, actual1); - Assert.Same(Schema, actual2); - Assert.Same(Schema, actual3); + Assert.Same(actual1, actual2); + Assert.Same(actual1, actual3); A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Id, CancellationToken)) .MustHaveHappenedOnceExactly(); } + [Fact] + public async Task Should_not_resolve_schema_by_id_and_name_if_cached_before_but_disabled() + { + options.CacheDuration = default; + + A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Id, CancellationToken)) + .ReturnsLazily(() => Schema with { Version = 3 }); + + var actual1 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Id, true, CancellationToken); + var actual2 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Id, true, CancellationToken); + var actual3 = await sut.GetSchemaAsync(AppId.Id, SchemaId.Name, true, CancellationToken); + + Assert.NotSame(actual1, actual2); + Assert.NotSame(actual1, actual3); + + A.CallTo(() => schemaRepository.FindAsync(AppId.Id, SchemaId.Id, CancellationToken)) + .MustHaveHappenedTwiceExactly(); + } + [Fact] public async Task Should_resolve_schemas() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index cbc60562a..12f404299 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -27,7 +27,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -39,7 +39,7 @@ - + all diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index e53f2366a..37c4dacb9 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs index ec15cfbc6..fd4bc0296 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs @@ -31,6 +31,36 @@ public class DefaultDomainObjectCacheTests sut = new DefaultDomainObjectCache(cache, serializer, distributedCache, options); } + [Fact] + public async Task Should_use_instance_with_zero_cache_duration() + { + var options = Options.Create(new DomainObjectCacheOptions + { + CacheDuration = default + }); + + var sut2 = new DefaultDomainObjectCache(cache, serializer, distributedCache, options); + + await sut2.SetAsync(id, 10, 20, ct); + + Assert.Equal(0, await sut2.GetAsync(id, 10, ct)); + } + + [Fact] + public async Task Should_use_instance_with_negative_cache_duration() + { + var options = Options.Create(new DomainObjectCacheOptions + { + CacheDuration = TimeSpan.FromMinutes(-10) + }); + + var sut2 = new DefaultDomainObjectCache(cache, serializer, distributedCache, options); + + await sut2.SetAsync(id, 10, 20, ct); + + Assert.Equal(0, await sut2.GetAsync(id, 10, ct)); + } + [Fact] public async Task Should_add_to_cache_and_memory_cache_on_set() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs index 606842dec..ab1f2772d 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs @@ -71,7 +71,7 @@ public class EventConsumerProcessorTests A.CallTo(() => eventConsumer.CanClear) .Returns(true); - A.CallTo(() => eventConsumer.Handles(A._)) + A.CallTo(() => eventConsumer.HandlesAsync(A._)) .Returns(true); A.CallTo(() => eventConsumer.On(A>>._)) @@ -324,7 +324,7 @@ public class EventConsumerProcessorTests [Fact] public async Task Should_not_invoke_but_update_position_if_consumer_does_not_want_to_handle() { - A.CallTo(() => eventConsumer.Handles(storedEvent)) + A.CallTo(() => eventConsumer.HandlesAsync(storedEvent)) .Returns(false); await sut.InitializeAsync(default); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs index 3c9f9b845..bbcf5fa4c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs @@ -9,7 +9,6 @@ using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries.Json; using Squidex.Infrastructure.TestHelpers; -using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; namespace Squidex.Infrastructure.Queries; diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 8fcd48eef..57d263e0f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,7 +25,7 @@ - + diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj index 14f5b5c32..03487c0f2 100644 --- a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/frontend/src/app/features/content/shared/forms/content-field.component.scss b/frontend/src/app/features/content/shared/forms/content-field.component.scss index a6221050e..53e178d0d 100644 --- a/frontend/src/app/features/content/shared/forms/content-field.component.scss +++ b/frontend/src/app/features/content/shared/forms/content-field.component.scss @@ -30,9 +30,9 @@ } &-buttons { - @include absolute(100%, 0); + @include absolute(100%, 7rem); margin: 0; - margin-top: -8px; + margin-top: -9px; overflow: hidden; } diff --git a/frontend/src/app/shared/components/chat-dialog.component.html b/frontend/src/app/shared/components/chat-dialog.component.html index ea4a688ef..8b43ff013 100644 --- a/frontend/src/app/shared/components/chat-dialog.component.html +++ b/frontend/src/app/shared/components/chat-dialog.component.html @@ -57,18 +57,16 @@
-
+
{{ 'chat.answer' | sqxTranslate}}
- + -
- -
+
@@ -110,7 +108,7 @@ [disabled]="snapshot.isRunning" />
-
diff --git a/frontend/src/app/shared/components/chat-dialog.component.scss b/frontend/src/app/shared/components/chat-dialog.component.scss index 15b1bfdcb..e63b13b11 100644 --- a/frontend/src/app/shared/components/chat-dialog.component.scss +++ b/frontend/src/app/shared/components/chat-dialog.component.scss @@ -52,6 +52,21 @@ textarea { } } +.use-container { + position: relative; + + .btn { + @include absolute(1rem, 1rem); + visibility: hidden; + } + + &:hover { + .btn { + visibility: visible; + } + } +} + @keyframes blink { 50% { fill: transparent diff --git a/frontend/src/app/shared/components/chat-dialog.component.ts b/frontend/src/app/shared/components/chat-dialog.component.ts index 43852f41b..f39e15bc8 100644 --- a/frontend/src/app/shared/components/chat-dialog.component.ts +++ b/frontend/src/app/shared/components/chat-dialog.component.ts @@ -9,7 +9,7 @@ import { NgFor, NgIf } from '@angular/common'; import { booleanAttribute, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { delay } from 'rxjs/operators'; -import { FocusOnInitDirective, ModalDialogComponent, ResizedDirective, ScrollActiveDirective, TooltipDirective, TranslatePipe } from '@app/framework'; +import { FocusOnInitDirective, MarkdownDirective, MathHelper, ModalDialogComponent, ResizedDirective, ScrollActiveDirective, TooltipDirective, TranslatePipe } from '@app/framework'; import { AppsState, AuthService, StatefulComponent, TranslationsService } from '@app/shared/internal'; import { UserIdPicturePipe } from './pipes'; @@ -32,6 +32,7 @@ interface State { imports: [ FocusOnInitDirective, FormsModule, + MarkdownDirective, ModalDialogComponent, NgFor, NgIf, @@ -43,6 +44,8 @@ interface State { ], }) export class ChatDialogComponent extends StatefulComponent { + private readonly conversationId = MathHelper.guid(); + @Output() public textSelect = new EventEmitter(); @@ -87,7 +90,7 @@ export class ChatDialogComponent extends StatefulComponent { isRunning: true, })); - this.translator.ask(this.appsState.appName, { prompt }).pipe(delay(500)) + this.translator.ask(this.appsState.appName, { prompt, conversationId: this.conversationId }).pipe(delay(500)) .subscribe({ next: chatAnswers => { if (chatAnswers.length === 0) { diff --git a/frontend/src/app/shared/services/translations.service.ts b/frontend/src/app/shared/services/translations.service.ts index d70c5b9eb..734784cc5 100644 --- a/frontend/src/app/shared/services/translations.service.ts +++ b/frontend/src/app/shared/services/translations.service.ts @@ -31,6 +31,9 @@ export type TranslateDto = Readonly<{ }>; export type AskDto = Readonly<{ + // Optional conversation ID. + conversationId?: string; + // The question to ask. prompt: string; }>; diff --git a/tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs b/tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs index 3aa5cbcee..d7469f77e 100644 --- a/tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs @@ -112,9 +112,6 @@ public class GraphQLSubscriptionTests : IClassFixture subscriptionStream.Where(x => x.Data.AssetChanges.Id == assetId).Timeout(TimeSpan.FromSeconds(30)) .FirstOrDefaultAsync(); - // Wait a little bit for the subscription to propagate. - await Task.Delay(2000); - // STEP 2: Create asset. var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); diff --git a/tools/TestSuite/docker-compose-base.yml b/tools/TestSuite/docker-compose-base.yml index 0787f60e2..f17d23a77 100644 --- a/tools/TestSuite/docker-compose-base.yml +++ b/tools/TestSuite/docker-compose-base.yml @@ -5,6 +5,7 @@ services: image: squidex-local environment: - ASPNETCORE_URLS=http://+:5000 + - CLUSTERING__RANDOMNAME=true - EVENTSTORE__MONGODB__CONFIGURATION=mongodb://mongo - GRAPHQL__CACHEDURATION=00:00:00 - IDENTITY__ADMINCLIENTID=root @@ -12,6 +13,7 @@ services: - IDENTITY__ADMINEMAIL=hello@squidex.io - IDENTITY__ADMINPASSWORD=1q2w3e$$R - IDENTITY__MULTIPLEDOMAINS=true + - MESSAGING__DATACACHEDURATION=00:00:00 - RULES__RULESCACHEDURATION=00:00:00 - SCRIPTING__TIMEOUTEXECUTION=00:00:10 - SCRIPTING__TIMEOUTSCRIPT=00:00:10 diff --git a/tools/TestSuite/docker-compose.yml b/tools/TestSuite/docker-compose.yml index 390c16012..b9dfc0544 100644 --- a/tools/TestSuite/docker-compose.yml +++ b/tools/TestSuite/docker-compose.yml @@ -22,6 +22,22 @@ services: # Hosted on path and separate worker squidex2: + extends: + file: docker-compose-base.yml + service: squidex_base + environment: + - CLUSTERING__WORKER=false + - EVENTSTORE__MONGODB__DATABASE=squidex2 + - STORE__MONGODB__CONTENTDATABASE=squidex2_content + - STORE__MONGODB__DATABASE=squidex2 + - STORE__MONGODB__TEXTHARDCOUNT=20 + - URLS__BASEPATH=squidex/ + - URLS__BASEURL=http://localhost:8081/squidex/ + depends_on: + - mongo + + # Hosted on path and separate worker + squidex2_worker: extends: file: docker-compose-base.yml service: squidex_base