Browse Source

Merge branch 'master' of https://github.com/Squidex/squidex

pull/1086/head
Sebastian 3 years ago
parent
commit
93d9852703
  1. 6
      .github/workflows/dev.yml
  2. 4
      .github/workflows/make-screenshots.yml
  3. 8
      .github/workflows/release.yml
  4. 4
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  5. 2
      backend/src/Migrations/Migrations.csproj
  6. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  7. 56
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs
  8. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs
  9. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs
  10. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintObjectConverter.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JsonMapper.cs
  12. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  13. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs
  14. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  15. 29
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/AssetSubscription.cs
  16. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/ContentSubscription.cs
  17. 86
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageEvaluator.cs
  18. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/EventMessageWrapper.cs
  19. 31
      backend/src/Squidex.Domain.Apps.Core.Operations/Subscriptions/SubscriptionPublisher.cs
  20. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  21. 14
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppCacheOptions.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs
  23. 27
      backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  24. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs
  25. 1
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  26. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs
  28. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  29. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs
  30. 27
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs
  31. 30
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  32. 26
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
  33. 1
      backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs
  34. 19
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  35. 13
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCacheOptions.cs
  36. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  37. 2
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  38. 2
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  39. 6
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  40. 8
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  41. 2
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  42. 19
      backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs
  43. 28
      backend/src/Squidex.Infrastructure/DomainId.cs
  44. 2
      backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs
  45. 28
      backend/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs
  46. 4
      backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumer.cs
  47. 13
      backend/src/Squidex.Infrastructure/IDeterministicHashCode.cs
  48. 16
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  49. 2
      backend/src/Squidex.Infrastructure/States/ShardedService.cs
  50. 8
      backend/src/Squidex.Infrastructure/States/Sharding.cs
  51. 2
      backend/src/Squidex.Shared/Squidex.Shared.csproj
  52. 1
      backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs
  53. 2
      backend/src/Squidex.Web/Squidex.Web.csproj
  54. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  55. 5
      backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs
  56. 16
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  57. 29
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  58. 2
      backend/src/Squidex/Config/Domain/FontendServices.cs
  59. 15
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  60. 9
      backend/src/Squidex/Config/Domain/StoreServices.cs
  61. 108
      backend/src/Squidex/Config/Messaging/MessagingServices.cs
  62. 42
      backend/src/Squidex/Squidex.csproj
  63. 21
      backend/src/Squidex/appsettings.json
  64. 2
      backend/src/Squidex/wwwroot/editor/squidex-editor.css
  65. 446
      backend/src/Squidex/wwwroot/editor/squidex-editor.js
  66. 142
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateConverterTests.cs
  67. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  68. 40
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/AssetSubscriptionTests.cs
  69. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/ContentSubscriptionTests.cs
  70. 96
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/EventMessageEvaluatorTests.cs
  71. 38
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Subscriptions/SubscriptionPublisherTests.cs
  72. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  73. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs
  74. 66
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs
  75. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  76. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs
  77. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs
  78. 45
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs
  79. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs
  80. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  81. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs
  82. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaFieldTests.cs
  83. 60
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs
  84. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  85. 53
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/ContentDomainObjectTests.Path_should_invoke_scripts.verified.txt
  86. 53
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/ContentDomainObjectTests.Update_should_invoke_scripts.verified.txt
  87. 2
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  88. 30
      backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs
  89. 2
      backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs
  90. 4
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs
  91. 1
      backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs
  92. 4
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  93. 8
      backend/tests/Squidex.Infrastructure.Tests/States/ShardedServiceTests.cs
  94. 4
      backend/tests/Squidex.Infrastructure.Tests/States/ShardingTests.cs
  95. 2
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
  96. 4
      frontend/src/app/features/content/shared/forms/content-field.component.scss
  97. 14
      frontend/src/app/shared/components/chat-dialog.component.html
  98. 15
      frontend/src/app/shared/components/chat-dialog.component.scss
  99. 7
      frontend/src/app/shared/components/chat-dialog.component.ts
  100. 3
      frontend/src/app/shared/services/translations.service.ts

6
.github/workflows/dev.yml

@ -25,7 +25,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Prepare - Setup Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Prepare - Setup Node
uses: actions/setup-node@v4.0.2
@ -33,7 +33,7 @@ jobs:
node-version: 18
- name: Build - BUILD
uses: docker/build-push-action@v5.2.0
uses: docker/build-push-action@v5.3.0
with:
load: true
build-args: "SQUIDEX__RUNTIME__VERSION=7.0.0-dev-${{ env.BUILD_NUMBER }}"
@ -130,7 +130,7 @@ jobs:
- name: Publish - Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

4
.github/workflows/make-screenshots.yml

@ -15,7 +15,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Prepare - Setup Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Prepare - Setup Node
uses: actions/setup-node@v4.0.2
@ -23,7 +23,7 @@ jobs:
node-version: 18
- name: Build - BUILD
uses: docker/build-push-action@v5.2.0
uses: docker/build-push-action@v5.3.0
with:
load: true
cache-from: type=gha

8
.github/workflows/release.yml

@ -20,7 +20,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Prepare - Setup Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
uses: docker/setup-buildx-action@v3.2.0
- name: Prepare - Setup Node
uses: actions/setup-node@v4.0.2
@ -28,7 +28,7 @@ jobs:
node-version: 18
- name: Build - BUILD
uses: docker/build-push-action@v5.2.0
uses: docker/build-push-action@v5.3.0
with:
load: true
build-args: "SQUIDEX__BUILD__VERSION=${{ env.GITHUB_REF_SLUG }},SQUIDEX__RUNTIME__VERSION=${{ env.GITHUB_REF_SLUG }}"
@ -131,7 +131,7 @@ jobs:
fi
- name: Publish - Login to Docker Hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@ -167,7 +167,7 @@ jobs:
- name: Release - Get Changelog Entry
id: changelog_reader
uses: mindsers/changelog-reader-action@v2.2.2
uses: mindsers/changelog-reader-action@v2.2.3
with:
version: ${{ env.GITHUB_REF_SLUG }}
path: ./CHANGELOG.md

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>

56
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs

@ -0,0 +1,56 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent;
public sealed class UpdateValues : IContentValueConverter
{
private readonly ContentData existingData;
private readonly IScriptEngine scriptEngine;
private ScriptVars? vars;
public UpdateValues(ContentData existingData, IScriptEngine scriptEngine)
{
this.existingData = existingData;
this.scriptEngine = scriptEngine;
}
public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent)
{
if (source.Value is not JsonObject jsonObject)
{
return (false, source);
}
if (jsonObject.TryGetValue("$unset", out var value1) && !Equals(value1.Value, false))
{
return (true, source);
}
if (!jsonObject.TryGetValue("$update", out var value2) || value2.Value is not string update)
{
return (false, source);
}
var options = new ScriptOptions { Readonly = true };
vars ??= new ScriptVars
{
["$data"] = existingData,
["$self"] = jsonObject
};
var result = scriptEngine.Execute(vars, update, options);
return (false, result);
}
}

1
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs

@ -7,6 +7,7 @@
using System.Diagnostics;
using Jint.Native;
using Squidex.Domain.Apps.Core.Scripting.Internal;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper;

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

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintObjectConverter.cs

@ -14,6 +14,7 @@ using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Scripting.Internal;
@ -44,6 +45,9 @@ public sealed class JintObjectConverter : IObjectConverter
case ClaimsPrincipal principal:
result = JintUser.Create(engine, principal);
return true;
case JsonValue jsonValue:
result = JsonMapper.Map(jsonValue, engine);
return true;
case DomainId domainId:
result = domainId.ToString();
return true;

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs → backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JsonMapper.cs

@ -14,7 +14,7 @@ using Jint.Runtime.Interop;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
namespace Squidex.Domain.Apps.Core.Scripting.Internal;
public static class JsonMapper
{

3
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -152,8 +152,9 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor
var engine = new Engine(engineOptions =>
{
engineOptions.SetTypeConverter(engine => new CustomClrConverter(engine));
engineOptions.AddObjectConverter(JintObjectConverter.Instance);
engineOptions.AllowClrWrite(!options.Readonly);
engineOptions.SetTypeConverter(engine => new CustomClrConverter(engine));
engineOptions.SetReferencesResolver(NullPropagation.Instance);
engineOptions.Strict();

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs

@ -14,4 +14,6 @@ public record struct ScriptOptions
public bool CanDisallow { get; set; }
public bool AsContext { get; set; }
public bool Readonly { get; set; }
}

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)

1
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs

@ -17,7 +17,6 @@ using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Domain.Apps.Core.Scripting.Internal;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Properties;

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

8
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -322,9 +322,11 @@ public partial class ContentDomainObject : DomainObject<WriteContent>
operation.MustHavePermission(PermissionIds.AppContentsUpdate);
operation.MustHaveData(c.Data);
var newData = operation.InvokeUpdates(c.Data, Snapshot.EditingData);
if (!c.DoNotValidate)
{
await operation.ValidateInputAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished, ct);
await operation.ValidateInputAsync(newData, c.OptimizeValidation, Snapshot.IsPublished, ct);
}
if (!c.DoNotValidateWorkflow)
@ -332,8 +334,6 @@ public partial class ContentDomainObject : DomainObject<WriteContent>
await operation.CheckUpdateAsync();
}
var newData = c.Data;
if (c.EnrichDefaults)
{
newData = operation.GenerateDefaultValues(newData);
@ -363,6 +363,8 @@ public partial class ContentDomainObject : DomainObject<WriteContent>
operation.MustHavePermission(PermissionIds.AppContentsUpdate);
operation.MustHaveData(c.Data);
c.Data = operation.InvokeUpdates(c.Data, Snapshot.EditingData);
if (!c.DoNotValidate)
{
await operation.ValidateInputPartialAsync(c.Data, c.OptimizeValidation, Snapshot.IsPublished, ct);

12
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs

@ -9,6 +9,7 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
@ -97,6 +98,17 @@ public static class ValidationExtensions
return converter.Convert(data);
}
public static ContentData InvokeUpdates(this ContentOperation operation, ContentData data, ContentData currentData)
{
var converter =
new ContentConverter(
operation.Components,
operation.Schema);
converter.Add(new UpdateValues(currentData, operation.Resolve<IScriptEngine>()));
return converter.Convert(data);
}
public static async Task CheckReferrersAsync(this ContentOperation operation,
CancellationToken ct)
{

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())

28
backend/src/Squidex.Infrastructure/DomainId.cs

@ -10,7 +10,7 @@ using System.ComponentModel;
namespace Squidex.Infrastructure;
[TypeConverter(typeof(DomainIdTypeConverter))]
public readonly struct DomainId : IEquatable<DomainId>, IComparable<DomainId>
public readonly struct DomainId : IEquatable<DomainId>, IComparable<DomainId>, IDeterministicHashCode
{
private static readonly string EmptyString = Guid.Empty.ToString();
@ -74,6 +74,32 @@ public readonly struct DomainId : IEquatable<DomainId>, IComparable<DomainId>
return id ?? EmptyString;
}
public int GetDeterministicHashCode()
{
unchecked
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
if (id != null)
{
for (int i = 0; i < id.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ id[i];
if (i == id.Length - 1)
{
break;
}
hash2 = ((hash2 << 5) + hash2) ^ id[i + 1];
}
}
return hash1 + (hash2 * 1566083941);
}
}
public int CompareTo(DomainId other)
{
return string.Compare(ToString(), other.ToString(), StringComparison.Ordinal);

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()

13
backend/src/Squidex.Infrastructure/IDeterministicHashCode.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure;
public interface IDeterministicHashCode
{
int GetDeterministicHashCode();
}

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.Infrastructure/States/ShardedService.cs

@ -9,7 +9,7 @@ using Squidex.Hosting;
namespace Squidex.Infrastructure.States;
public abstract class ShardedService<TKey, TService> : IInitializable where TKey : notnull
public abstract class ShardedService<TKey, TService> : IInitializable where TKey : notnull, IDeterministicHashCode
{
private readonly Dictionary<string, TService> shards = [];
private readonly IShardingStrategy sharding;

8
backend/src/Squidex.Infrastructure/States/Sharding.cs

@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States;
public interface IShardingStrategy
{
string GetShardKey<T>(T key) where T : notnull;
string GetShardKey<T>(T key) where T : notnull, IDeterministicHashCode;
IEnumerable<string> GetShardKeys();
}
@ -24,7 +24,7 @@ public sealed class SingleSharding : IShardingStrategy
{
}
public string GetShardKey<T>(T key) where T : notnull
public string GetShardKey<T>(T key) where T : notnull, IDeterministicHashCode
{
return string.Empty;
}
@ -44,9 +44,9 @@ public sealed class PartitionedSharding : IShardingStrategy
this.numPartitions = numPartitions;
}
public string GetShardKey<T>(T key) where T : notnull
public string GetShardKey<T>(T key) where T : notnull, IDeterministicHashCode
{
var partition = Math.Abs(key.GetHashCode()) % numPartitions;
var partition = Math.Abs(key.GetDeterministicHashCode()) % numPartitions;
return GetShardKey(partition);
}

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

29
backend/src/Squidex/Config/Domain/CommandsServices.cs

@ -40,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>();
@ -139,4 +147,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>();

108
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)
@ -59,16 +61,22 @@ public static class MessagingServices
services.AddSingletonAs<UsageTrackerWorker>()
.AsSelf().As<IMessageHandler>();
}
services.AddSingletonAs<BackupJob>()
.As<IJobRunner>();
if (isRandomName)
{
services.AddSingletonAs<RandomInstanceNameProvider>()
.As<IInstanceNameProvider>();
}
services.AddSingletonAs<RestoreJob>()
.As<IJobRunner>();
services.AddSingletonAs<BackupJob>()
.As<IJobRunner>();
services.AddSingletonAs<RuleRunnerJob>()
.As<IJobRunner>();
}
services.AddSingletonAs<RestoreJob>()
.As<IJobRunner>();
services.AddSingletonAs<RuleRunnerJob>()
.As<IJobRunner>();
services.AddSingleton<IMessagingSerializer>(c =>
new SystemTextJsonMessagingSerializer(c.GetRequiredService<JsonSerializerOptions>()));
@ -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"
}
},

2
backend/src/Squidex/wwwroot/editor/squidex-editor.css

@ -1 +1 @@
.remirror-theme{width:100%}.remirror-theme{position:relative}.remirror-theme *{box-sizing:border-box}.remirror-theme .ProseMirror p{margin-bottom:1rem!important}.remirror-editor{min-height:300px!important;max-height:500px}.MuiStack-root>.MuiBox-root{margin-right:5px}.MuiBox-root>.MuiButtonBase-root{border-color:#dedfe3!important;border-radius:0}.MuiBox-root>.MuiButtonBase-root{border-left-width:0!important}.MuiBox-root>.MuiBox-root:first-child>.MuiButtonBase-root{border-left-width:1px!important}.MuiStack-root>.MuiBox-root>.MuiButtonBase-root:first-child{border-left-width:1px!important}.MuiButtonBase-root.Mui-selected:hover{background-color:#3284f4!important}.remirror-editor-wrapper{padding-top:0!important}.custom-icon path{fill:#0000008a}.remirror-theme div.ProseMirror{border:1px solid #dedfe3!important;border-radius:0!important;box-shadow:none!important}.MuiTooltip-popper>div{background-color:#1a2129;border-radius:0;font-size:85%;font-weight:400;padding:.5rem}.MuiMenu-paper{transform:none!important}.squidex-editor-disabled{pointer-events:none}.squidex-editor-menu{border:1px solid #dedfe3;border-bottom:0;padding:5px}.squidex-editor-menu fieldset{border-radius:0;border:0;display:flex;outline:0;padding:0}.squidex-editor-menu fieldset:disabled,.squidex-editor-menu fieldset.disabled{pointer-events:none}.squidex-editor-menu fieldset:disabled,.squidex-editor-menu fieldset.disabled{opacity:.5}.squidex-editor-counter{border:1px solid #dedfe3;border-top:0;font-size:85%;font-weight:400;opacity:.8;padding:4px 10px 4px 4px;text-align:right}.squidex-editor-image-view{border:1px solid #dedfe3;border-radius:0;display:inline-block;margin-top:15px;margin-bottom:15px;overflow:hidden}.squidex-editor-image-buttons{bottom:10px;left:10px;position:absolute}.squidex-editor-image-element{display:block;max-width:400px;max-height:400px}.squidex-editor-image-info{left:10px;top:10px;position:absolute;background-color:#3284f4;color:#fff;font-size:85%;font-weight:400;padding:2px 6px}.squidex-editor-main{position:relative}.squidex-editor-main .ace_editor{bottom:0;left:0;right:0;top:0;position:absolute;border:1px solid #dedfe3}.squidex-editor-content-link{align-items:center;border-radius:2px;border:1px solid #dedfe3;padding:10px;display:flex;flex-direction:row;flex-wrap:nowrap;margin-top:10px;margin-bottom:10px}.squidex-editor-content-schema{display:block;overflow-x:hidden;overflow-y:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;min-width:0;width:auto;border-right:1px solid #c2c4cc;color:#8b8f9d;flex-shrink:0;padding-left:10px;padding-right:10px;width:200px}.squidex-editor-content-name{display:block;overflow-x:hidden;overflow-y:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;min-width:0;width:auto;padding-left:10px;padding-right:0}.squidex-editor-html{margin-bottom:10px;margin-top:10px;position:relative}.squidex-editor-html-label{left:6px;top:0;position:absolute;color:#8b8f9d;font-size:85%;font-weight:400}.squidex-editor-html textarea{font-family:monospace;padding:30px 20px 20px}.squidex-editor-button{align-items:center;background-color:#fff;border-radius:0;border:1px solid #dedfe3;bottom:10px;display:inline-flex;font-size:85%;font-weight:400;margin-right:5px;padding:6px 12px}.squidex-editor-button:hover{background-color:#f5f5f5}.squidex-editor-input{border:1px solid #dedfe3;border-radius:0;box-sizing:border-box;height:30px;margin-left:0;margin-right:5px;outline:none;padding:6px 12px}.squidex-editor-input:active{border-color:#3284f4}.squidex-editor-floating{border:1px solid #dedfe3}.squidex-editor-floating .MuiBox-root{margin-right:0!important}.squidex-editor-floating .MuiButtonBase-root{height:30px}.squidex-editor-modal-wrapper,.squidex-editor-modal-backdrop{bottom:0;left:0;right:0;top:0;position:absolute}.squidex-editor-modal-backdrop{background-color:#00000003}.squidex-editor-modal-window{left:50%;top:50%;background-color:#fff;border:0;border-radius:.25rem;box-sizing:border-box;box-shadow:0 3px 16px #0003;margin-top:-50px;margin-left:-150px;position:absolute;width:350px}.squidex-editor-modal-body{display:flex;flex-direction:row;flex-grow:1;padding-top:0!important}.squidex-editor-modal-body,.squidex-editor-modal-title{padding:15px}.squidex-editor-modal-title{font-size:85%}.squidex-editor-modal-window input{flex-grow:1}.b:before{content:"<a>";font-family:monospace;font-size:90%}.b:after{content:"</a>";font-family:monospace;font-size:90%}
.remirror-theme{width:100%}.remirror-theme{position:relative}.remirror-theme *{box-sizing:border-box}.remirror-theme .ProseMirror p{margin-bottom:1rem!important}.remirror-editor{min-height:300px!important;max-height:500px}.MuiStack-root>.MuiBox-root{margin-right:5px}.MuiBox-root>.MuiButtonBase-root{border-color:#dedfe3!important;border-radius:0}.MuiBox-root>.MuiButtonBase-root{border-left-width:0!important}.MuiBox-root>.MuiBox-root:first-child>.MuiButtonBase-root{border-left-width:1px!important}.MuiStack-root>.MuiBox-root>.MuiButtonBase-root:first-child{border-left-width:1px!important}.MuiButtonBase-root.Mui-selected:hover{background-color:#3284f4!important}.remirror-editor-wrapper{padding-top:0!important}.custom-icon path{fill:#0000008a}.remirror-theme div.ProseMirror{border:1px solid #dedfe3!important;border-radius:0!important;box-shadow:none!important}.MuiTooltip-popper>div{background-color:#1a2129;border-radius:0;font-size:85%;font-weight:400;padding:.5rem}.MuiMenu-paper{transform:none!important}.squidex-editor-disabled{pointer-events:none}.squidex-editor-menu{border:1px solid #dedfe3;border-bottom:0;padding:5px}.squidex-editor-menu fieldset{border-radius:0;border:0;display:flex;outline:0;padding:0}.squidex-editor-menu fieldset:disabled,.squidex-editor-menu fieldset.disabled{pointer-events:none}.squidex-editor-menu fieldset:disabled,.squidex-editor-menu fieldset.disabled{opacity:.5}.squidex-editor-counter{border:1px solid #dedfe3;border-top:0;font-size:85%;font-weight:400;opacity:.8;padding:4px 10px 4px 4px;text-align:right}.squidex-editor-image-view{border:1px solid #dedfe3;border-radius:0;display:inline-block;margin-top:15px;margin-bottom:15px;overflow:hidden}.squidex-editor-image-buttons{bottom:10px;left:10px;position:absolute}.squidex-editor-image-element{display:block;max-width:400px;max-height:400px}.squidex-editor-image-info{left:10px;top:10px;position:absolute;background-color:#3284f4;color:#fff;font-size:85%;font-weight:400;padding:2px 6px}.squidex-editor-main{position:relative}.squidex-editor-main .ace_editor{bottom:0;left:0;right:0;top:0;position:absolute;border:1px solid #dedfe3}.squidex-editor-content-link{align-items:center;border-radius:2px;border:1px solid #dedfe3;padding:10px;display:flex;flex-direction:row;flex-wrap:nowrap;margin-top:10px;margin-bottom:10px}.squidex-editor-content-schema{display:block;overflow-x:hidden;overflow-y:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;min-width:0;width:auto;border-right:1px solid #c2c4cc;color:#8b8f9d;flex-shrink:0;padding-left:10px;padding-right:10px;width:200px}.squidex-editor-content-name{display:block;overflow-x:hidden;overflow-y:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;min-width:0;width:auto;padding-left:10px;padding-right:0}.squidex-editor-html{margin-bottom:10px;margin-top:10px;position:relative}.squidex-editor-html-label{left:6px;top:0;position:absolute;color:#8b8f9d;font-size:85%;font-weight:400}.squidex-editor-html textarea{border:1px solid #dedfe3;font-family:monospace;font-size:inherit;height:100px;outline:none;overflow-x:hidden;overflow-y:auto;padding:30px 14px 14px;resize:vertical;width:100%}.squidex-editor-button{align-items:center;background-color:#fff;border-radius:0;border:1px solid #dedfe3;bottom:10px;display:inline-flex;font-size:85%;font-weight:400;margin-right:5px;padding:6px 12px}.squidex-editor-button:hover{background-color:#f5f5f5}.squidex-editor-input{border:1px solid #dedfe3;border-radius:0;box-sizing:border-box;height:30px;margin-left:0;margin-right:5px;outline:none;padding:6px 12px}.squidex-editor-input:active{border-color:#3284f4}.squidex-editor-floating{border:1px solid #dedfe3}.squidex-editor-floating .MuiBox-root{margin-right:0!important}.squidex-editor-floating .MuiButtonBase-root{height:30px}.squidex-editor-modal-wrapper,.squidex-editor-modal-backdrop{bottom:0;left:0;right:0;top:0;position:absolute}.squidex-editor-modal-backdrop{background-color:#00000003}.squidex-editor-modal-window{left:50%;top:50%;background-color:#fff;border:0;border-radius:.25rem;box-sizing:border-box;box-shadow:0 3px 16px #0003;margin-top:-50px;margin-left:-150px;position:absolute;width:350px}.squidex-editor-modal-body{display:flex;flex-direction:row;flex-grow:1;padding-top:0!important}.squidex-editor-modal-body,.squidex-editor-modal-title{padding:15px}.squidex-editor-modal-title{font-size:85%}.squidex-editor-modal-window input{flex-grow:1}.b:before{content:"<a>";font-family:monospace;font-size:90%}.b:after{content:"</a>";font-family:monospace;font-size:90%}

446
backend/src/Squidex/wwwroot/editor/squidex-editor.js

File diff suppressed because one or more lines are too long

142
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateConverterTests.cs

@ -0,0 +1,142 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Operations.ConvertContent;
public class UpdateConverterTests
{
private readonly IScriptEngine scriptEngine;
public UpdateConverterTests()
{
scriptEngine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())),
Options.Create(new JintScriptOptions
{
TimeoutScript = TimeSpan.FromSeconds(2),
TimeoutExecution = TimeSpan.FromSeconds(10)
}));
}
[Fact]
public void Should_update()
{
var field1 = Fields.Number(1, "number1", Partitioning.Invariant);
var schema =
new Schema { Name = "my-schema" }
.AddField(field1);
var source =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en",
JsonValue.Object()
.Add("$update", "$data.number1.en + 1")));
var existing =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en", 42));
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine))
.Convert(source);
var expected =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en", 43));
Assert.Equal(expected, actual);
}
[Fact]
public void Should_update_with_existing_value()
{
var field1 = Fields.Number(1, "number1", Partitioning.Invariant);
var schema =
new Schema { Name = "my-schema" }
.AddField(field1);
var source =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en",
JsonValue.Object()
.Add("$update", "$data.number1.en + $self.increment")
.Add("increment", 7)));
var existing =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en", 42));
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine))
.Convert(source);
var expected =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en", 49));
Assert.Equal(expected, actual);
}
[Fact]
public void Should_unset_value()
{
var field1 = Fields.Number(1, "number1", Partitioning.Invariant);
var schema =
new Schema { Name = "my-schema" }
.AddField(field1);
var source =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en",
JsonValue.Object()
.Add("$unset", true)));
var existing =
new ContentData()
.AddField(field1.Name,
new ContentFieldData()
.AddLocalized("en", 42));
var actual =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new UpdateValues(existing, scriptEngine))
.Convert(source);
var expected =
new ContentData()
.AddField(field1.Name,
new ContentFieldData());
Assert.Equal(expected, actual);
}
}

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" />

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;

45
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs

@ -45,6 +45,18 @@ public class ContentDomainObjectTests : HandlerTestBase<WriteContent>
.AddField("my-field1",
new ContentFieldData()
.AddInvariant(1));
private readonly ContentData update =
new ContentData()
.AddField("my-field1",
new ContentFieldData()
.AddInvariant(
JsonValue.Object()
.Add("$update", "$data['my-field1'].iv + 42")));
private readonly ContentData updated =
new ContentData()
.AddField("my-field1",
new ContentFieldData()
.AddInvariant(43));
private readonly ContentData patch =
new ContentData()
.AddField("my-field2",
@ -85,6 +97,9 @@ public class ContentDomainObjectTests : HandlerTestBase<WriteContent>
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), CancellationToken))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<DataScriptVars>(0)!.Data!));
A.CallTo(() => scriptEngine.Execute(A<ScriptVars>._, A<string>._, A<ScriptOptions>._))
.Returns(JsonValue.Create(43));
A.CallTo(() => contentWorkflow.GetInitialStatusAsync(Schema))
.Returns(Status.Draft);
@ -352,6 +367,21 @@ public class ContentDomainObjectTests : HandlerTestBase<WriteContent>
.MustHaveHappened();
}
[Fact]
public async Task Update_should_invoke_scripts()
{
var command = new UpdateContent { Data = update };
await ExecuteCreateAsync();
var actual = await PublishAsync(command);
await VerifySutAsync(actual);
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(updated, data, Status.Draft), "<update-script>", ScriptOptions(), CancellationToken))
.MustHaveHappened();
}
[Fact]
public async Task Update_should_create_events_and_update_new_version_if_draft_available()
{
@ -409,6 +439,21 @@ public class ContentDomainObjectTests : HandlerTestBase<WriteContent>
.MustHaveHappened();
}
[Fact]
public async Task Path_should_invoke_scripts()
{
var command = new PatchContent { Data = update };
await ExecuteCreateAsync();
var actual = await PublishAsync(command);
await VerifySutAsync(actual);
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(updated, data, Status.Draft), "<update-script>", ScriptOptions(), CancellationToken))
.MustHaveHappened();
}
[Fact]
public async Task Patch_should_create_events_and_update_new_version_if_draft_available()
{

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>

53
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/ContentDomainObjectTests.Path_should_invoke_scripts.verified.txt

@ -0,0 +1,53 @@
{
sut: {
UniqueId: Guid_1--Guid_2,
Snapshot: {
SchemaId: Guid_3,my-schema,
CurrentVersion: {
Status: Draft,
Data: {
my-field1: {
iv: 43.0
}
}
},
EditingData: {
my-field1: {
iv: 43.0
}
},
EditingStatus: Draft,
IsPublished: false,
AppId: Guid_1,my-app,
IsDeleted: false,
UniqueId: Guid_1--Guid_2,
Id: Guid_2,
CreatedBy: subject:me,
LastModifiedBy: subject:me,
Version: 1
},
Version: 1,
ObjectState: Created
},
events: [
{
Headers: {
AggregateId: Guid_1--Guid_2,
EventId: Guid_4
},
Payload: {
Data: {
my-field1: {
iv: 43.0
}
},
NewVersion: false,
ContentId: Guid_2,
SchemaId: Guid_3,my-schema,
AppId: Guid_1,my-app,
Actor: subject:me,
FromRule: false
}
}
]
}

53
backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/ContentDomainObjectTests.Update_should_invoke_scripts.verified.txt

@ -0,0 +1,53 @@
{
sut: {
UniqueId: Guid_1--Guid_2,
Snapshot: {
SchemaId: Guid_3,my-schema,
CurrentVersion: {
Status: Draft,
Data: {
my-field1: {
iv: 43.0
}
}
},
EditingData: {
my-field1: {
iv: 43.0
}
},
EditingStatus: Draft,
IsPublished: false,
AppId: Guid_1,my-app,
IsDeleted: false,
UniqueId: Guid_1--Guid_2,
Id: Guid_2,
CreatedBy: subject:me,
LastModifiedBy: subject:me,
Version: 1
},
Version: 1,
ObjectState: Created
},
events: [
{
Headers: {
AggregateId: Guid_1--Guid_2,
EventId: Guid_4
},
Payload: {
Data: {
my-field1: {
iv: 43.0
}
},
NewVersion: false,
ContentId: Guid_2,
SchemaId: Guid_3,my-schema,
AppId: Guid_1,my-app,
Actor: subject:me,
FromRule: false
}
}
]
}

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

2
backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs

@ -138,10 +138,12 @@ public class DomainIdTests
Assert.Equal(domainId_1_a, domainId_1_b);
Assert.Equal(domainId_1_a.GetHashCode(), domainId_1_b.GetHashCode());
Assert.Equal(domainId_1_a.GetDeterministicHashCode(), domainId_1_b.GetDeterministicHashCode());
Assert.True(domainId_1_a.Equals((object)domainId_1_b));
Assert.NotEqual(domainId_1_a, domainId_2_a);
Assert.NotEqual(domainId_1_a.GetHashCode(), domainId_2_a.GetHashCode());
Assert.NotEqual(domainId_1_a.GetDeterministicHashCode(), domainId_2_a.GetDeterministicHashCode());
Assert.False(domainId_1_a.Equals((object)domainId_2_a));
Assert.True(domainId_1_a == domainId_1_b);

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" />

8
backend/tests/Squidex.Infrastructure.Tests/States/ShardedServiceTests.cs

@ -19,14 +19,14 @@ public class ShardedServiceTests
{
}
private class TestSut : ShardedService<int, IInner>
private class TestSut : ShardedService<DomainId, IInner>
{
public TestSut(IShardingStrategy sharding, Func<string, IInner> factory)
: base(sharding, factory)
{
}
public IInner ExposeShard(int key)
public IInner ExposeShard(DomainId key)
{
return Shard(key);
}
@ -76,7 +76,7 @@ public class ShardedServiceTests
[Fact]
public void Should_provide_shards()
{
Assert.Equal(inner1, sut.ExposeShard(0));
Assert.Equal(inner2, sut.ExposeShard(1));
Assert.Equal(inner1, sut.ExposeShard(DomainId.Create("2")));
Assert.Equal(inner2, sut.ExposeShard(DomainId.Create("3")));
}
}

4
backend/tests/Squidex.Infrastructure.Tests/States/ShardingTests.cs

@ -16,7 +16,7 @@ public class ShardingTests
for (var i = 0; i < 1000; i++)
{
var shardKey = strategy.GetShardKey(Guid.NewGuid());
var shardKey = strategy.GetShardKey(DomainId.NewGuid());
Assert.Equal(string.Empty, shardKey);
}
@ -39,7 +39,7 @@ public class ShardingTests
for (var i = 0; i < 1000; i++)
{
var shardKey = strategy.GetShardKey(Guid.NewGuid());
var shardKey = strategy.GetShardKey(DomainId.NewGuid());
Assert.True(shardKey is "_0" or "_1" or "_2");
}

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

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

Loading…
Cancel
Save