Browse Source

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.
pull/1084/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
bab6d23cdf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  2. 2
      backend/src/Migrations/Migrations.csproj
  3. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  4. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs
  5. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  6. 29
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs
  7. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs
  8. 86
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs
  9. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs
  10. 31
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  12. 14
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppCacheOptions.cs
  13. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs
  14. 27
      backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  15. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs
  16. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs
  18. 27
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs
  19. 30
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  20. 26
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
  21. 1
      backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs
  22. 19
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  23. 13
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCacheOptions.cs
  24. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  25. 2
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  26. 2
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  27. 6
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  28. 8
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  29. 2
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  30. 19
      backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs
  31. 2
      backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs
  32. 28
      backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs
  33. 4
      backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs
  34. 16
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  35. 2
      backend/src/Squidex.Shared/Squidex.Shared.csproj
  36. 1
      backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs
  37. 2
      backend/src/Squidex.Web/Squidex.Web.csproj
  38. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  39. 5
      backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs
  40. 16
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  41. 30
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  42. 2
      backend/src/Squidex/Config/Domain/FontendServices.cs
  43. 15
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  44. 9
      backend/src/Squidex/Config/Domain/StoreServices.cs
  45. 94
      backend/src/Squidex/Config/Messaging/MessagingServices.cs
  46. 42
      backend/src/Squidex/Squidex.csproj
  47. 21
      backend/src/Squidex/appsettings.json
  48. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  49. 40
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs
  50. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs
  51. 96
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs
  52. 38
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs
  53. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  54. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs
  55. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs
  56. 66
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs
  57. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  58. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs
  59. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs
  60. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs
  61. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  62. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs
  63. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs
  64. 60
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs
  65. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  66. 2
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  67. 30
      backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs
  68. 4
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs
  69. 1
      backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs
  70. 4
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  71. 2
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
  72. 4
      frontend/src/app/features/content/shared/forms/content-field.component.scss
  73. 14
      frontend/src/app/shared/components/chat-dialog.component.html
  74. 15
      frontend/src/app/shared/components/chat-dialog.component.scss
  75. 7
      frontend/src/app/shared/components/chat-dialog.component.ts
  76. 3
      frontend/src/app/shared/services/translations.service.ts
  77. 3
      tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs
  78. 2
      tools/TestSuite/docker-compose-base.yml
  79. 16
      tools/TestSuite/docker-compose.yml

4
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -16,10 +16,10 @@
<PackageReference Include="Confluent.SchemaRegistry.Serdes.Avro" Version="2.3.0" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="7.17.5" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="5.1.0" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="5.2.0" />
<PackageReference Include="Google.Cloud.Logging.V2" Version="4.2.0" />
<PackageReference Include="Google.Cloud.Monitoring.V3" Version="3.6.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Migrations/Migrations.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -12,7 +12,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

14
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)
{

7
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -18,17 +18,18 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fluid.Core" Version="2.5.0" />
<PackageReference Include="Fluid.Core" Version="2.7.0" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.0.1" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="NJsonSchema" Version="11.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.4.1" />
<PackageReference Include="Squidex.AI" Version="6.6.4" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.6.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

29
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<bool> ShouldHandle(object message)
public ValueTask<bool> ShouldHandle(object message)
{
return new ValueTask<bool>(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);
}
}

8
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<bool> ShouldHandle(object message)
public ValueTask<bool> ShouldHandle(object message)
{
return new ValueTask<bool>(ShouldHandleCore(message));
}

86
backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs

@ -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<DomainId, Dictionary<Guid, AppSubscription>> subscriptions = [];
private readonly ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
public async ValueTask<IEnumerable<Guid>> GetSubscriptionsAsync(object message)
{
if (message is not AppEvent appEvent)
{
return Enumerable.Empty<Guid>();
}
readerWriterLock.EnterReadLock();
try
{
List<Guid>? 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<Guid>();
}
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();
}
}
}

12
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<ISubscriptionEventCreator> subscriptionEventCreators;
private readonly IEnumerable<ISubscriptionEventCreator> creators;
public Envelope<AppEvent> Event { get; }
object IPayloadWrapper.Message => Event.Payload;
public EventMessageWrapper(Envelope<AppEvent> @event, IEnumerable<ISubscriptionEventCreator> subscriptionEventCreators)
public EventMessageWrapper(Envelope<AppEvent> @event, IEnumerable<ISubscriptionEventCreator> creators)
{
Event = @event;
this.subscriptionEventCreators = subscriptionEventCreators;
this.creators = creators;
}
public async ValueTask<object> 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;
}

31
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<ISubscriptionEventCreator> subscriptionEventCreators;
private readonly IEnumerable<ISubscriptionEventCreator> subscriptionCreators;
public string Name => "Subscriptions";
@ -24,26 +27,36 @@ public sealed class SubscriptionPublisher : IEventConsumer
public bool CanClear => false;
public SubscriptionPublisher(ISubscriptionService subscriptionService, IEnumerable<ISubscriptionEventCreator> subscriptionEventCreators)
public SubscriptionPublisher(ISubscriptionService subscriptionService,
IEnumerable<ISubscriptionEventCreator> subscriptionCreators)
{
this.subscriptionService = subscriptionService;
this.subscriptionEventCreators = subscriptionEventCreators;
this.subscriptionCreators = subscriptionCreators;
}
public bool Handles(StoredEvent @event)
public async ValueTask<bool> HandlesAsync(StoredEvent @event)
{
return subscriptionService.HasSubscriptions;
var key = @event.StreamName.Split(DomainId.IdSeparator)[0];
return await subscriptionService.HasSubscriptionsAsync(key);
}
public Task On(Envelope<IEvent> @event)
{
if (@event.Payload is not AppEvent)
if (@event.Payload is AssetEvent assetEvent)
{
return Task.CompletedTask;
var wrapper = new EventMessageWrapper(@event.To<AppEvent>(), subscriptionCreators);
return subscriptionService.PublishAsync($"asset-{assetEvent.AppId.Id}", wrapper);
}
var wrapper = new EventMessageWrapper(@event.To<AppEvent>(), subscriptionEventCreators);
if (@event.Payload is ContentEvent contentEvent)
{
var wrapper = new EventMessageWrapper(@event.To<AppEvent>(), subscriptionCreators);
return subscriptionService.PublishAsync($"content-{contentEvent.AppId.Id}", wrapper);
}
return subscriptionService.PublishAsync(wrapper);
return Task.CompletedTask;
}
}

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

14
backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AppSubscription.cs → 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<bool> ShouldHandle(object message);
public TimeSpan CacheDuration { get; set; }
}

4
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<bool> HandlesAsync(StoredEvent @event)
{
return consumingTypes.Contains(@event.Data.Type);
return new ValueTask<bool>(consumingTypes.Contains(@event.Data.Type));
}
public async Task On(Envelope<IEvent> @event)

27
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<NameReservationState.State> persistenceFactory)
IPersistenceFactory<NameReservationState.State> persistenceFactory,
IOptions<AppCacheOptions> 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<string?> 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<App> 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<string, object?>(GetCacheKey(app.Id), app),
new KeyValuePair<string, object?>(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[]
{

4
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<bool> HandlesAsync(StoredEvent @event)
{
return consumingTypes.Contains(@event.Data.Type);
return new ValueTask<bool>(consumingTypes.Contains(@event.Data.Type));
}
public async Task On(Envelope<IEvent> @event)

4
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<bool> HandlesAsync(StoredEvent @event)
{
return consumingTypes.Contains(@event.Data.Type);
return new ValueTask<bool>(consumingTypes.Contains(@event.Data.Type));
}
public async Task On(Envelope<IEvent> @event)

2
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);
});

27
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<object>(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<EnrichedAssetEventType?>("type")
Type = fieldContext.GetArgument<EnrichedAssetEventType?>("type")
};
var observable =
await context.Resolve<ISubscriptionService>()
.SubscribeAsync(key, subscription, fieldContext.CancellationToken);
return observable.OfType<EnrichedAssetEvent>();
});
}
}

30
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<object>(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<EnrichedContentEventType?>("type"),
Type = fieldContext.GetArgument<EnrichedContentEventType?>("type"),
// The name of the schema is used instead of the ID for a simpler API.
SchemaName = c.GetArgument<string?>("schemaName")
SchemaName = fieldContext.GetArgument<string?>("schemaName"),
};
var observable =
await context.Resolve<ISubscriptionService>()
.SubscribeAsync(key, subscription, fieldContext.CancellationToken);
return observable.OfType<EnrichedContentEvent>();
});
}

26
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<IResolveFieldContext, AppSubscription> action)
{
return new SourceStreamResolver<object>(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<ISubscriptionService>().Subscribe<object>(subscription);
});
}
}

1
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;

19
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<NameReservationState.State> persistenceFactory;
private readonly SchemaCacheOptions options;
public SchemasIndex(ISchemaRepository schemaRepository, IReplicatedCache schemaCache,
IPersistenceFactory<NameReservationState.State> persistenceFactory)
IPersistenceFactory<NameReservationState.State> persistenceFactory,
IOptions<SchemaCacheOptions> options)
{
this.schemaRepository = schemaRepository;
this.schemaCache = schemaCache;
this.persistenceFactory = persistenceFactory;
this.options = options.Value;
}
public async Task<List<Schema>> 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<string, object?>(GetCacheKey(schema.AppId.Id, schema.Id), schema),
new KeyValuePair<string, object?>(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[]
{

13
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; }
}

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

@ -27,7 +27,7 @@
<PackageReference Include="CsvHelper" Version="31.0.2" />
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.DataLoader" Version="7.8.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -19,7 +19,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

6
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -18,13 +18,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityModel" Version="6.2.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.3" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="5.2.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="5.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SharpPwned.NET" Version="2.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

8
backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj

@ -11,11 +11,11 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="23.2.0" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="23.2.0" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.2.0" />
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="23.2.1" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="23.2.1" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.2.1" />
<PackageReference Include="Grpc.Net.Client" Version="2.61.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

19
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<T> GetAsync<T>(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<T>(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())

2
backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs

@ -53,7 +53,7 @@ internal sealed class ParseSubscription : IEventSubscriber<StoredEvent>, IEventS
{
Envelope<IEvent>? @event = null;
if (eventConsumer.Handles(storedEvent))
if (await eventConsumer.HandlesAsync(storedEvent))
{
@event = eventFormatter.ParseIfKnown(storedEvent);
}

28
backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs

@ -7,37 +7,19 @@
namespace Squidex.Infrastructure.EventSourcing;
public class Envelope<T> where T : class, IEvent
public sealed class Envelope<T>(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<TOther> To<TOther>() where TOther : class, IEvent
{
return new Envelope<TOther>((payload as TOther)!, headers.CloneHeaders());
return new Envelope<TOther>((payload as TOther)!, Headers.CloneHeaders());
}
public static implicit operator Envelope<IEvent>(Envelope<T> source)
{
return source == null ? source! : new Envelope<IEvent>(source.payload, source.headers);
return source == null ? source! : new Envelope<IEvent>(source.Payload, source.Headers);
}
}

4
backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs

@ -21,9 +21,9 @@ public interface IEventConsumer
bool CanClear => true;
bool Handles(StoredEvent @event)
ValueTask<bool> HandlesAsync(StoredEvent @event)
{
return true;
return new ValueTask<bool>(true);
}
Task ClearAsync()

16
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -13,23 +13,23 @@
<ItemGroup>
<PackageReference Include="MailKit" Version="4.4.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="8.0.3" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.20.0" />
<PackageReference Include="NodaTime" Version="3.1.11" />
<PackageReference Include="OpenTelemetry.Api" Version="1.7.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="6.4.1" />
<PackageReference Include="Squidex.Caching" Version="6.4.1" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.4.1" />
<PackageReference Include="Squidex.Log" Version="6.4.1" />
<PackageReference Include="Squidex.Messaging" Version="6.4.1" />
<PackageReference Include="Squidex.Text" Version="6.4.1" />
<PackageReference Include="Squidex.Assets" Version="6.6.4" />
<PackageReference Include="Squidex.Caching" Version="6.6.4" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.6.4" />
<PackageReference Include="Squidex.Log" Version="6.6.4" />
<PackageReference Include="Squidex.Messaging" Version="6.6.4" />
<PackageReference Include="Squidex.Text" Version="6.6.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

2
backend/src/Squidex.Shared/Squidex.Shared.csproj

@ -10,7 +10,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

1
backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs

@ -37,7 +37,6 @@ public sealed class SetupMiddleware
else
{
isUserFound = true;
await next(context);
}
}

2
backend/src/Squidex.Web/Squidex.Web.csproj

@ -16,7 +16,7 @@
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.7.1" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
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;

5
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
{
/// <summary>
/// Optional conversation ID.
/// </summary>
public string? ConversationId { get; set; }
/// <summary>
/// The text to ask.
/// </summary>

16
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<IActionResult> 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);
}
}

30
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<RestrictAppsOptions>(config,
"usage");
services.Configure<AppCacheOptions>(config,
"caching:apps");
services.Configure<DomainObjectCacheOptions>(config,
"caching:domainObjects");
services.Configure<SchemaCacheOptions>(config,
"caching:schemas");
services.ConfigureForObsoleteReplicatedCacheSetting(config);
services.AddSingletonAs<InMemoryCommandBus>()
.As<ICommandBus>();
@ -135,4 +144,25 @@ public static class CommandsServices
services.AddSingletonAs<DefaultDomainObjectCache>()
.As<IDomainObjectCache>();
}
private static void ConfigureForObsoleteReplicatedCacheSetting(this IServiceCollection services, IConfiguration config)
{
var isCaching = config.GetValue<bool>("caching:replicated:enable");
services.Configure<AppCacheOptions>(options =>
{
if (options.CacheDuration == default && isCaching)
{
options.CacheDuration = TimeSpan.FromMinutes(5);
}
});
services.Configure<SchemaCacheOptions>(options =>
{
if (options.CacheDuration == default && isCaching)
{
options.CacheDuration = TimeSpan.FromMinutes(5);
}
});
}
}

2
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;

15
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<ExposedConfiguration>(config,
"exposedConfiguration");
services.Configure<ReplicatedCacheOptions>(config,
"caching:replicated");
services.Configure<JintScriptOptions>(config,
"scripting");
@ -133,6 +130,16 @@ public static class InfrastructureServices
services.AddSingletonAs<LanguagesInitializer>()
.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);

9
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<IMongoClient>();

94
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<MessagingOptions>(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<bool>("caching:replicated:enable");
var isRandomName = config.GetValue<bool>("clustering:randomName");
var isWorker = config.GetValue<bool>("clustering:worker");
if (isWorker)
@ -61,6 +63,12 @@ public static class MessagingServices
.AsSelf().As<IMessageHandler>();
}
if (isRandomName)
{
services.AddSingletonAs<RandomInstanceNameProvider>()
.As<IInstanceNameProvider>();
}
services.AddSingletonAs<BackupJob>()
.As<IJobRunner>();
@ -76,55 +84,43 @@ public static class MessagingServices
services.AddSingletonAs<SubscriptionPublisher>()
.As<IEventConsumer>();
services.AddSingletonAs<EventMessageEvaluator>()
.As<IMessageEvaluator>();
services.AddSingletonAs<DefaultJobService>()
.As<IJobService>().As<IDeleter>();
services.AddReplicatedCacheMessaging(isCaching, options =>
{
options.TransportSelector = (transport, _) => transport.First(x => x is NullTransport != isCaching);
});
services.Configure<SubscriptionOptions>(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;
});
}
}

42
backend/src/Squidex/Squidex.csproj

@ -38,44 +38,44 @@
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.3" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.RulesetToEditorconfigConverter" Version="3.3.3" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.20.0" />
<PackageReference Include="MongoDB.Driver" Version="2.24.0" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.OpenTelemetry" Version="1.0.0" />
<PackageReference Include="NetTopologySuite.IO.GeoJSON4STJ" Version="4.0.0" />
<PackageReference Include="NJsonSchema" Version="11.0.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.1.2" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.3" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.4" />
<PackageReference Include="OpenCover" Version="4.7.1221" PrivateAssets="all" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.7.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.7.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.2.2" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.4.1" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.4.1" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.4.1" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.4.1" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.4.1" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.4.1" />
<PackageReference Include="Squidex.Assets.S3" Version="6.4.1" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.4.1" />
<PackageReference Include="ReportGenerator" Version="5.2.4" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.6.4" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.6.4" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.6.4" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.6.4" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.6.4" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.6.4" />
<PackageReference Include="Squidex.Assets.S3" Version="6.6.4" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.6.4" />
<PackageReference Include="Squidex.ClientLibrary" Version="19.1.0" />
<PackageReference Include="Squidex.Hosting" Version="6.4.1" />
<PackageReference Include="Squidex.Messaging.All" Version="6.4.1" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.4.1" />
<PackageReference Include="Squidex.Hosting" Version="6.6.4" />
<PackageReference Include="Squidex.Messaging.All" Version="6.6.4" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.6.4" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.3.0" />

21
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"
}
},

6
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<TranslationsFixture>
[Fact]
public async Task Should_generate_content()
{
A.CallTo(() => chatAgent.PromptAsync(A<string>._, "prompt", A<CancellationToken>._))
A.CallTo(() => chatAgent.PromptAsync("prompt", A<string>._, A<CancellationToken>._))
.Returns(ChatBotResponse.Success("Generated"));
var vars = new ScriptVars
@ -637,7 +637,7 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
Assert.Equal("Generated", actual.ToString());
A.CallTo(() => chatAgent.StopConversationAsync(A<string>._, A<CancellationToken>._))
.MustHaveHappened();
.MustNotHaveHappened();
}
[Theory]

40
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;
}
}

6
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);

96
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs

@ -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<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly NamedId<DomainId> 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;
}
}

38
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<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly ISubscriptionService subscriptionService = A.Fake<ISubscriptionService>();
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<object>._, 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<object>._, default))
.MustHaveHappened();
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.1.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NetTopologySuite.IO.GeoJSON4STJ" Version="4.0.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.1.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.7.0" />

10
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<DomainId> componentId1 = NamedId.Of(DomainId.NewGuid(), "my-schema");
private readonly NamedId<DomainId> 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()
{

12
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<IEvent, AppDeleted>();
@ -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<IEvent, AppContributorRemoved>();
@ -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<IEvent, AppCreated>();
@ -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]

66
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs

@ -28,17 +28,19 @@ public class AppsIndexTests : GivenContext
private readonly TestState<NameReservationState.State> state;
private readonly IAppRepository appRepository = A.Fake<IAppRepository>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly AppCacheOptions options = new AppCacheOptions();
private readonly AppsIndex sut;
public AppsIndexTests()
{
options.CacheDuration = TimeSpan.FromMinutes(5);
state = new TestState<NameReservationState.State>("Apps");
var replicatedCache =
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>(),
Options.Create(new ReplicatedCacheOptions { Enable = true }));
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>());
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()
{

1
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;

8
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<IEvent, AssetDeleted>();
@ -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<IEvent, AssetCreated>();
@ -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]

1
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;

4
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<object>(A<AssetSubscription>._))
A.CallTo(() => subscriptionService.SubscribeAsync($"asset-{TestApp.Default.Id}", A<AssetSubscription>._, default))
.Returns(stream);
var actual = await ExecuteAsync(new TestQuery
@ -128,7 +128,7 @@ public class GraphQLSubscriptionTests : GraphQLTestBase
.AddInvariant(42))
});
A.CallTo(() => subscriptionService.Subscribe<object>(A<ContentSubscription>._))
A.CallTo(() => subscriptionService.SubscribeAsync($"content-{TestApp.Default.Id}", A<ContentSubscription>._, default))
.Returns(stream);
var actual = await ExecuteAsync(new TestQuery

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -95,7 +95,7 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
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();

1
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;

1
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

60
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs

@ -26,17 +26,19 @@ public class SchemasIndexTests : GivenContext
private readonly TestState<NameReservationState.State> state;
private readonly ISchemaRepository schemaRepository = A.Fake<ISchemaRepository>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly SchemaCacheOptions options = new SchemaCacheOptions();
private readonly SchemasIndex sut;
public SchemasIndexTests()
{
options.CacheDuration = TimeSpan.FromMinutes(5);
state = new TestState<NameReservationState.State>($"{AppId.Id}_Schemas");
var replicatedCache =
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>(),
Options.Create(new ReplicatedCacheOptions { Enable = true }));
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), A.Fake<IMessageBus>());
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()
{

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -27,7 +27,7 @@
<PackageReference Include="GraphQL" Version="7.8.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
<PackageReference Include="Lorem.Universal.Net" Version="4.0.80" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -39,7 +39,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Reactive.Linq" Version="6.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Verify.Xunit" Version="23.2.2" />
<PackageReference Include="Verify.Xunit" Version="23.5.2" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PrivateAssets>all</PrivateAssets>

2
backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.1.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

30
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<int>(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<int>(id, 10, ct));
}
[Fact]
public async Task Should_add_to_cache_and_memory_cache_on_set()
{

4
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<StoredEvent>._))
A.CallTo(() => eventConsumer.HandlesAsync(A<StoredEvent>._))
.Returns(true);
A.CallTo(() => eventConsumer.On(A<IEnumerable<Envelope<IEvent>>>._))
@ -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);

1
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;

4
backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.1.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -25,7 +25,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.1.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.7.0" />

2
backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.1.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

4
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;
}

14
frontend/src/app/shared/components/chat-dialog.component.html

@ -57,18 +57,16 @@
</div>
</div>
<div class="col">
<div class="bubble bubble-right">
<div class="bubble bubble-right use-container">
<div class="mb-2">
{{ 'chat.answer' | sqxTranslate}}
</div>
<textarea class="form-control" readonly [ngModel]="item.text"></textarea>
<span [sqxMarkdown]="item.text" optional="true" inline="false"></span>
<div class="mt-2">
<button type="button" class="btn btn-primary" (click)="textSelect.emit(item.text)">
{{ 'chat.use' | sqxTranslate }}
</button>
</div>
<button type="button" class="btn btn-secondary" (click)="textSelect.emit(item.text)">
{{ 'chat.use' | sqxTranslate }}
</button>
</div>
</div>
</div>
@ -110,7 +108,7 @@
[disabled]="snapshot.isRunning" />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-secondary" [disabled]="snapshot.isRunning">
<button type="submit" class="btn btn-primary" [disabled]="snapshot.isRunning">
{{ 'chat.ask' | sqxTranslate }}
</button>
</div>

15
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

7
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<State> {
private readonly conversationId = MathHelper.guid();
@Output()
public textSelect = new EventEmitter<string | undefined | null>();
@ -87,7 +90,7 @@ export class ChatDialogComponent extends StatefulComponent<State> {
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) {

3
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;
}>;

3
tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs

@ -112,9 +112,6 @@ public class GraphQLSubscriptionTests : IClassFixture<ContentFixture>
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");

2
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

16
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

Loading…
Cancel
Save