From 6b7c6fc8a6df01cac9e26bde575621c06b491e67 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 21 Jul 2023 13:40:12 +0200 Subject: [PATCH] GraphQL Data loader fix. (#1006) * GraphQL Data loader fix. * Fix tests * Fix event store. * Simplify tests --- .../Actions/Script/ScriptActionHandler.cs | 22 +- .../Squidex.Extensions.csproj | 2 +- .../Squidex.Translator.csproj | 2 +- backend/src/Migrations/Migrations.csproj | 2 +- .../EnrichedEvents/EnrichedAssetEvent.cs | 2 +- .../Squidex.Domain.Apps.Core.Model.csproj | 2 +- .../HandleRules/RuleEventFormatter.cs | 12 +- .../Scripting/AssetCommandScriptVars.cs | 22 +- .../Scripting/AssetEntityScriptVars.cs | 33 +- .../Scripting/AssetScriptVars.cs | 19 +- .../Scripting/ContentScriptVars.cs | 30 +- .../Scripting/ContentWrapper/JsonMapper.cs | 7 + .../Scripting/DataScriptVars.cs | 2 +- .../Scripting/EventScriptVars.cs | 8 +- .../Internal/AssetMetadataWrapper.cs | 2 +- .../Scripting/Internal/JintExtensions.cs | 2 +- .../Scripting/JintScriptEngine.cs | 13 +- .../Scripting/ScriptContext.cs | 93 ---- .../Scripting/ScriptExecutionContext.cs | 2 +- .../Scripting/ScriptVars.cs | 109 +++- .../Scripting/WritableContext.cs | 2 +- ...Squidex.Domain.Apps.Core.Operations.csproj | 4 +- .../ValidateContent/JsonValueConverter.cs | 28 +- ...ongoAssetFolderRepository_SnapshotStore.cs | 5 +- .../MongoAssetRepository_SnapshotStore.cs | 5 +- ...quidex.Domain.Apps.Entities.MongoDb.csproj | 2 +- .../Apps/DomainObject/AppCommandMiddleware.cs | 8 +- .../Assets/AssetsFluidExtension.cs | 109 ++-- .../Assets/AssetsJintExtension.cs | 207 ++++---- .../Assets/Commands/UploadAssetCommand.cs | 2 + .../Assets/DefaultAssetFileStore.cs | 6 + .../DomainObject/AssetCommandMiddleware.cs | 14 +- .../Guards/ScriptMetadataWrapper.cs | 124 ----- .../Guards/ScriptingExtensions.cs | 41 +- .../Assets/IAssetFileStore.cs | 3 + .../Assets/ImageAssetMetadataSource.cs | 12 +- .../Assets/Queries/Steps/ScriptAsset.cs | 3 +- .../Assets/Transformations.cs | 69 ++- .../Contents/Counter/CounterJintExtension.cs | 4 +- .../GraphQL/Cache/CachingBatchLoader.cs | 70 +++ .../Cache/CachingDataLoaderExtensions.cs | 36 ++ .../GraphQL/Cache/EmptyDataLoaderResult.cs | 25 + .../GraphQL/Cache/NonCachingBatchLoader.cs | 34 ++ .../GraphQL/GraphQLExecutionContext.cs | 126 ++--- .../Contents/GraphQL/GraphQLOptions.cs | 2 + .../GraphQL/Types/Assets/AssetActions.cs | 7 +- .../GraphQL/Types/Contents/ContentActions.cs | 17 +- .../GraphQL/Types/Contents/ContentFields.cs | 14 +- .../GraphQL/Types/Contents/FieldVisitor.cs | 14 +- .../Contents/Queries/ContentQueryService.cs | 2 +- .../Contents/Queries/QueryExecutionContext.cs | 4 - .../Contents/ReferencesJintExtension.cs | 4 +- .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 3 + .../Squidex.Domain.Apps.Entities.csproj | 2 +- .../Squidex.Domain.Apps.Events.csproj | 2 +- .../Squidex.Domain.Users.MongoDb.csproj | 2 +- .../Squidex.Domain.Users.csproj | 2 +- .../Squidex.Infrastructure.Azure.csproj | 2 +- ...quidex.Infrastructure.GetEventStore.csproj | 2 +- .../MongoEventStoreSubscription.cs | 5 +- .../EventSourcing/MongoEventStore_Reader.cs | 4 +- .../Log/MongoRequestLogRepository.cs | 4 +- .../MongoDb/MongoExtensions.cs | 2 +- .../Squidex.Infrastructure.MongoDb.csproj | 2 +- .../Caching/IQueryCache.cs | 8 +- .../Caching/QueryCache.cs | 79 +-- .../ListDictionary.KeyCollection.cs | 125 ----- .../ListDictionary.ValueCollection.cs | 125 ----- .../Collections/ListDictionary.cs | 273 ---------- .../Queries/Json/ValueConverter.cs | 4 +- .../Squidex.Infrastructure.csproj | 2 +- .../Squidex.Web/Pipeline/SchemaResolver.cs | 2 +- backend/src/Squidex.Web/Squidex.Web.csproj | 2 +- .../Controllers/Apps/AppImageController.cs | 8 +- .../Assets/AssetContentController.cs | 12 +- .../Assets/Models/AssetContentQueryDto.cs | 8 +- .../Controllers/Schemas/SchemasController.cs | 2 +- .../Controllers/Profile/ProfileController.cs | 8 +- .../Squidex/Config/Domain/AssetServices.cs | 2 +- .../Squidex/Config/Domain/ContentsServices.cs | 2 +- backend/src/Squidex/Config/Web/WebServices.cs | 11 + backend/src/Squidex/Squidex.csproj | 2 +- backend/src/Squidex/appsettings.json | 3 + .../Scripting/AssetMetadataWrapperTests.cs} | 11 +- .../Scripting/JintScriptEngineTests.cs | 5 +- .../Scripting/ScriptingCompleterTests.cs | 1 + .../ValidateContent/ComponentFieldTests.cs | 13 + .../Squidex.Domain.Apps.Core.Tests.csproj | 2 +- .../DomainObject/AppCommandMiddlewareTests.cs | 8 +- .../Assets/AssetsFluidExtensionTests.cs | 14 +- .../Assets/AssetsJintExtensionTests.cs | 62 ++- .../Assets/DefaultAssetFileStoreTests.cs | 13 +- .../Assets/ImageAssetMetadataSourceTests.cs | 24 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 2 +- .../Squidex.Domain.Users.Tests.csproj | 2 +- .../Caching/QueryCacheTests.cs | 164 +----- .../Collections/ListDictionaryTests.cs | 478 ------------------ .../Squidex.Infrastructure.Tests.csproj | 2 +- .../Squidex.Web.Tests.csproj | 2 +- .../GenerateLanguages.csproj | 2 +- .../TestSuite.ApiTests/AssetTests.cs | 57 ++- .../TestSuite.ApiTests/BackupTests.cs | 10 +- .../TestSuite.ApiTests/ContentQueryTests.cs | 12 +- .../TestSuite.ApiTests/HistoryTests.cs | 4 +- .../TestSuite.ApiTests/RuleRunnerTests.cs | 138 ++--- .../TestSuite.ApiTests/SearchTests.cs | 8 +- .../TestSuite.ApiTests.csproj | 2 +- ...ould_compute_blur_hash_script.verified.txt | 49 ++ .../TestSuite.LoadTests.csproj | 2 +- .../TestSuite.Shared/ClientExtensions.cs | 165 ++++-- .../Fixtures/WebhookCatcherClient.cs | 27 - .../TestSuite.Shared/TestSuite.Shared.csproj | 2 +- .../webhook-catcher/docker-compose.yml | 13 + 114 files changed, 1366 insertions(+), 2036 deletions(-) delete mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingBatchLoader.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingDataLoaderExtensions.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/EmptyDataLoaderResult.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/NonCachingBatchLoader.cs delete mode 100644 backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs delete mode 100644 backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs delete mode 100644 backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs rename backend/tests/{Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs => Squidex.Domain.Apps.Core.Tests/Operations/Scripting/AssetMetadataWrapperTests.cs} (89%) delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs create mode 100644 tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_compute_blur_hash_script.verified.txt create mode 100644 tools/TestSuite/webhook-catcher/docker-compose.yml diff --git a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs index a0921ea89..98b4a7c3c 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs @@ -5,9 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Security.Claims; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Shared; +using Squidex.Shared.Identity; #pragma warning disable MA0048 // File name must match type name @@ -36,13 +39,30 @@ public sealed class ScriptActionHandler : RuleActionHandler net7.0 - 10.0 + 11.0 enable enable diff --git a/backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj b/backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj index 175da3681..0bfffc3b4 100644 --- a/backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj +++ b/backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj @@ -3,7 +3,7 @@ Exe net7.0 - 10.0 + 11.0 enable NU1608 diff --git a/backend/src/Migrations/Migrations.csproj b/backend/src/Migrations/Migrations.csproj index 639e53508..458c4d6e8 100644 --- a/backend/src/Migrations/Migrations.csproj +++ b/backend/src/Migrations/Migrations.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs index a5f14368f..311578d08 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs @@ -65,7 +65,7 @@ public sealed class EnrichedAssetEvent : EnrichedUserEventBase, IEnrichedEntityE public AssetType AssetType { get; set; } [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] - public AssetMetadata Metadata { get; } + public AssetMetadata Metadata { get; set; } [FieldDescription(nameof(FieldDescriptions.AssetIsImage))] public bool IsImage diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index cd3df41a9..778c209f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -2,7 +2,7 @@ net7.0 Squidex.Domain.Apps.Core - 10.0 + 11.0 enable en enable diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 70587d8ad..b3b70f156 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -23,11 +23,11 @@ using ValueTaskSupplement; namespace Squidex.Domain.Apps.Core.HandleRules; -public class RuleEventFormatter +public partial class RuleEventFormatter { private const string GlobalFallback = "null"; - private static readonly Regex RegexPatternOld = new Regex(@"^(?(?[^_]*)_(?[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private static readonly Regex RegexPatternNew = new Regex(@"^\{(?(?[\w]+)_(?[\w\.\-]+))[\s]*(\|[\s]*(?[^\?}]+))?(\?[\s]*(?[^\}\s]+))?[\s]*\}", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex RegexPatternOld = RegexPatternOldFactory(); + private static readonly Regex RegexPatternNew = RegexPatternNewFactory(); private readonly IJsonSerializer serializer; private readonly IEnumerable formatters; private readonly ITemplateEngine templateEngine; @@ -392,4 +392,10 @@ public class RuleEventFormatter return false; } + + [GeneratedRegex(@"^(?(?[^_]*)_(?[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture)] + private static partial Regex RegexPatternOldFactory(); + + [GeneratedRegex(@"^\{(?(?[\w]+)_(?[\w\.\-]+))[\s]*(\|[\s]*(?[^\?}]+))?(\?[\s]*(?[^\}\s]+))?[\s]*\}", RegexOptions.Compiled | RegexOptions.ExplicitCapture)] + private static partial Regex RegexPatternNewFactory(); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs index 7405a9a14..972315f90 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs @@ -16,66 +16,66 @@ public sealed class AssetCommandScriptVars : ScriptVars [FieldDescription(nameof(FieldDescriptions.AssetParentId))] public DomainId ParentId { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] public string? FileHash { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetFileName))] public string? FileName { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetSlug))] public string? FileSlug { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] public string? MimeType { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetParentPath))] public Array? ParentPath { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] public AssetMetadata? Metadata { - set => SetValue(value != null ? new AssetMetadataWrapper(value) : null); + set => SetInitial(value != null ? new AssetMetadataWrapper(value) : null); } [FieldDescription(nameof(FieldDescriptions.AssetTags))] public HashSet? Tags { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] public long FileSize { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] public bool? IsProtected { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))] public bool? Permanent { - set => SetValue(value); + set => SetInitial(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs index b70516eca..698a3631f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs @@ -17,66 +17,77 @@ public sealed class AssetEntityScriptVars : ScriptVars [FieldDescription(nameof(FieldDescriptions.AssetParentId))] public DomainId ParentId { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetFileHash))] public string? FileHash { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetFileName))] public string? FileName { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetSlug))] public string? FileSlug { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetMimeType))] public string? MimeType { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetParentPath))] public Array? ParentPath { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetMetadata))] public AssetMetadata? Metadata { - set => SetValue(value != null ? new ReadOnlyDictionary(value) : null); + set => SetInitial(value != null ? new ReadOnlyDictionary(value) : null); } [FieldDescription(nameof(FieldDescriptions.AssetTags))] public HashSet? Tags { - set => SetValue(value != null ? new ReadOnlyCollection(value.ToList()) : null); + set => SetInitial(value != null ? new ReadOnlyCollection(value.ToList()) : null); } [FieldDescription(nameof(FieldDescriptions.AssetFileSize))] public long FileSize { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetFileVersion))] public long FileVersion { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AssetIsProtected))] public bool? IsProtected { - set => SetValue(value); + set => SetInitial(value); + } + + [FieldDescription(nameof(FieldDescriptions.AssetType))] + public AssetType Type + { + set => SetInitial(value); + } + + public string? FileId + { + set => SetInitial(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs index beaf238ff..2b3e21598 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs @@ -15,42 +15,47 @@ public sealed class AssetScriptVars : ScriptVars [FieldDescription(nameof(FieldDescriptions.AppId))] public DomainId AppId { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.EntityId))] public DomainId AssetId { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AppName))] public string AppName { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.Operation))] public string Operation { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.Command))] public AssetCommandScriptVars Command { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.Asset))] public AssetEntityScriptVars Asset { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.User))] public ClaimsPrincipal? User { - set => SetValue(value); + set => SetInitial(value); + } + + public string? FileId + { + set => SetInitial(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs index 3d967ecaf..29a3b18ef 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs @@ -16,91 +16,91 @@ public sealed class ContentScriptVars : DataScriptVars [FieldDescription(nameof(FieldDescriptions.ContentValidate))] public Action Validate { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AppId))] public DomainId AppId { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.SchemaId))] public DomainId SchemaId { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.EntityId))] public DomainId ContentId { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.AppName))] public string AppName { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.ContentSchemaName))] public string SchemaName { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.Operation))] public string Operation { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))] public bool Permanent { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.User))] public ClaimsPrincipal? User { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.ContentStatus))] public Status Status { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.ContentStatusOld))] public Status StatusOld { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.ContentStatusOld))] public Status OldStatus { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.ContentData))] public ContentData? DataOld { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.ContentDataOld))] public ContentData? OldData { - set => SetValue(value); + set => SetInitial(value); } [FieldDescription(nameof(FieldDescriptions.ContentData))] public override ContentData? Data { get => GetValue(); - set => SetValue(value); + set => SetInitial(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs index a1f8a1040..be41c6a10 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -5,10 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections; using System.Globalization; using Jint; using Jint.Native; using Jint.Native.Object; +using Jint.Runtime.Interop; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; @@ -125,6 +127,11 @@ public static class JsonMapper return result; } + if (value is ObjectWrapper wrapper && wrapper.Target is not IDictionary) + { + return JsonValue.Create(wrapper.Target); + } + if (value.IsObject()) { var obj = value.AsObject(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs index d2a409ae0..ec31bdeb1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs @@ -14,6 +14,6 @@ public class DataScriptVars : ScriptVars public virtual ContentData? Data { get => GetValue(); - set => SetValue(value); + set => SetInitial(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs index 75cbb210e..ade92811d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs @@ -15,21 +15,21 @@ public sealed class EventScriptVars : ScriptVars { public DomainId AppId { - set => SetValue(value); + set => SetInitial(value); } public string AppName { - set => SetValue(value); + set => SetInitial(value); } public ClaimsPrincipal User { - set => SetValue(value); + set => SetInitial(value); } public EnrichedEvent Event { - set => SetValue(value); + set => SetInitial(value); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs index d25e9b93b..c034fd56d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Scripting.Internal; -internal sealed class AssetMetadataWrapper : IDictionary +public sealed class AssetMetadataWrapper : IDictionary { private readonly AssetMetadata metadata; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs index 6eedf84fd..bb4f3f51b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs @@ -76,7 +76,7 @@ public static class JintExtensions { foreach (var (key, item) in vars) { - engine.SetValue(key, item.Value!); + engine.SetValue(key, item); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index a91690c6c..ad1b7fa0f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -190,18 +190,23 @@ public sealed class JintScriptEngine : IScriptEngine, IScriptDescriptor private static Exception MapException(Exception inner) { + static Exception BuildException(string errorKey, string message, Exception? inner = null) + { + return new ValidationException(T.Get(errorKey, new { message }), inner); + } + switch (inner) { case ArgumentException: - return new ValidationException(T.Get("common.jsParseError", new { error = inner.Message })); + return BuildException("common.jsParseError", inner.Message); case JavaScriptException: - return new ValidationException(T.Get("common.jsError", new { message = inner.Message })); + return BuildException("common.jsError", inner.Message); case ParserException: - return new ValidationException(T.Get("common.jsError", new { message = inner.Message })); + return BuildException("common.jsError", inner.Message); case DomainException: return inner; default: - return new ValidationException(T.Get("common.jsError", new { message = inner.GetType().Name }), inner); + return BuildException("common.jsError", inner.GetType().Name, inner); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs deleted file mode 100644 index b689e17f9..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using Squidex.Infrastructure; -using Squidex.Text; - -namespace Squidex.Domain.Apps.Core.Scripting; - -public class ScriptContext : IEnumerable> -{ - private readonly Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public void CopyFrom(ScriptVars vars) - { - Guard.NotNull(vars); - - foreach (var (key, item) in vars) - { - if (!values.ContainsKey(key)) - { - SetItem(key, item); - } - } - } - - public void SetItem(string? key, (object? Value, bool IsReadonly) item) - { - Set(key, item.Value, item.IsReadonly); - } - - public void Set(string? key, object? value, bool isReadonly = false) - { - if (string.IsNullOrWhiteSpace(key)) - { - return; - } - - var finalKey = key.ToCamelCase(); - - if (values.TryGetValue(finalKey, out var existing) && existing.IsReadonly) - { - return; - } - - values[finalKey] = (value, isReadonly); - } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) - { - Guard.NotNull(key); - - value = default!; - - if (values.TryGetValue(key, out var item)) - { - value = item.Value; - return true; - } - - return false; - } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out T value) - { - Guard.NotNull(key); - - value = default!; - - if (values.TryGetValue(key, out var item) && item.Value is T typed) - { - value = typed; - return true; - } - - return false; - } - - public IEnumerator> GetEnumerator() - { - return values.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return values.GetEnumerator(); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs index fed2dc043..c320bac71 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Core.Scripting; -public abstract class ScriptExecutionContext : ScriptContext +public abstract class ScriptExecutionContext : ScriptVars { public Engine Engine { get; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs index 03bd8913d..844030b6d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs @@ -5,12 +5,34 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Squidex.Infrastructure; +using Squidex.Text; namespace Squidex.Domain.Apps.Core.Scripting; -public class ScriptVars : ScriptContext +public class ScriptVars : IReadOnlyDictionary { + private readonly Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly HashSet lockedKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + + public IEnumerable Keys + { + get => values.Keys; + } + + public IEnumerable Values + { + get => values.Values; + } + + public int Count + { + get => values.Count; + } + public object? this[string key] { get @@ -18,12 +40,78 @@ public class ScriptVars : ScriptContext TryGetValue(key, out var result); return result; } - set => Set(key, value, true); + set + { + Set(key, value); + } + } + + public void CopyFrom(ScriptVars vars) + { + Guard.NotNull(vars); + + foreach (var (key, item) in vars.values) + { + if (!values.ContainsKey(key)) + { + Set(key, item, vars.lockedKeys.Contains(key)); + } + } + } + + public void Set(string? key, object? value, bool isReadonly = false) + { + if (string.IsNullOrWhiteSpace(key)) + { + return; + } + + var finalKey = key.ToCamelCase(); + + if (lockedKeys.Contains(finalKey)) + { + return; + } + + values[finalKey] = value; + + if (isReadonly) + { + lockedKeys.Add(finalKey); + } + else + { + lockedKeys.Remove(finalKey); + } + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) + { + Guard.NotNull(key); + + values.TryGetValue(key, out value); + return true; } - public void SetValue(object? value, [CallerMemberName] string? key = null) + public bool TryGetValueIfExists(string key, [MaybeNullWhen(false)] out T value) + { + Guard.NotNull(key); + + value = default!; + + if (values.TryGetValue(key, out var item) && item is T typed) + { + value = typed; + return true; + } + + return false; + } + + public ScriptVars SetInitial(object? value, [CallerMemberName] string? key = null) { Set(key, value, true); + return this; } public T GetValue([CallerMemberName] string? key = null) @@ -35,4 +123,19 @@ public class ScriptVars : ScriptContext return default!; } + + public bool ContainsKey(string key) + { + return values.ContainsKey(key); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return values.GetEnumerator(); + } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs index 2c3542406..a04b3a255 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs @@ -22,7 +22,7 @@ internal sealed class WritableContext : ObjectInstance foreach (var (key, item) in vars) { - base.Set(key, FromObject(engine, item.Value), this); + base.Set(key, FromObject(engine, item), this); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 76a41d427..4a6dc3952 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -2,7 +2,7 @@ net7.0 Squidex.Domain.Apps.Core - 10.0 + 11.0 enable en enable @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 19bb0adfd..89c0eea0c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -256,22 +256,33 @@ public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonErro var id = DomainId.Empty; - if (o.TryGetValue("schemaName", out var found) && found.Value is string schemaName) + if (o.TryGetValue(Component.Descriptor, out var found) && found.Value is string schemaName) { id = components.FirstOrDefault(x => x.Value.Name == schemaName).Key; - - o.Remove("schemaName"); - o[Component.Discriminator] = id; } else if (o.TryGetValue(Component.Discriminator, out found) && found.Value is string discriminator) { - id = DomainId.Create(discriminator); + if (Guid.TryParseExact(discriminator, "D", out _)) + { + id = DomainId.Create(discriminator); + } + else + { + var componentEntry = components.FirstOrDefault(x => x.Value.Name == discriminator); + + if (componentEntry.Value != null) + { + id = componentEntry.Key; + } + else + { + id = DomainId.Create(discriminator); + } + } } else if (allowedIds?.Count == 1) { id = allowedIds[0]; - - o[Component.Discriminator] = id; } if (id == default) @@ -286,6 +297,9 @@ public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonErro var data = new JsonObject(o); + o[Component.Discriminator] = id; + + data.Remove(Component.Descriptor); data.Remove(Component.Discriminator); return (new Component(id.ToString(), data, schema), null); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs index d3af6212a..106e86d92 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs @@ -27,8 +27,9 @@ public sealed partial class MongoAssetFolderRepository : ISnapshotStore> ISnapshotStore.ReadAllAsync( CancellationToken ct) { - return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) - .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version, true)); + var documents = Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct); + + return documents.Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version, true)); } async Task> ISnapshotStore.ReadAsync(DomainId key, diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index a5809d3a1..db0c6bacf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -27,8 +27,9 @@ public sealed partial class MongoAssetRepository : ISnapshotStore> ISnapshotStore.ReadAllAsync( CancellationToken ct) { - return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) - .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version)); + var documents = Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct); + + return documents.Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version)); } async Task> ISnapshotStore.ReadAsync(DomainId key, diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index 93bf05b07..c030c99e5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs index 97eb4089c..2918f8281 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs @@ -16,15 +16,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject; public sealed class AppCommandMiddleware : AggregateCommandMiddleware { private readonly IAppImageStore appImageStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IAssetThumbnailGenerator assetGenerator; private readonly IContextProvider contextProvider; public AppCommandMiddleware(IDomainObjectFactory domainObjectFactory, - IAppImageStore appImageStore, IAssetThumbnailGenerator assetThumbnailGenerator, IContextProvider contextProvider) + IAppImageStore appImageStore, IAssetThumbnailGenerator assetGenerator, IContextProvider contextProvider) : base(domainObjectFactory) { this.appImageStore = appImageStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; + this.assetGenerator = assetGenerator; this.contextProvider = contextProvider; } @@ -57,7 +57,7 @@ public sealed class AppCommandMiddleware : AggregateCommandMiddleware { - if (input is not ObjectValue objectValue) - { - return ErrorNoAsset; - } - - async Task ResolveAssetTextAsync(AssetRef asset) - { - if (asset.FileSize > 256_000) - { - return ErrorTooBig; - } - - var assetFileStore = serviceProvider.GetRequiredService(); - - var encoding = arguments.At(0).ToStringValue()?.ToUpperInvariant(); - var encoded = await asset.GetTextAsync(encoding, assetFileStore, default); - - return new StringValue(encoded); - } + TryGetAssetRef(input, out var asset); - switch (objectValue.ToObjectValue()) - { - case IAssetEntity asset: - return await ResolveAssetTextAsync(asset.ToRef()); - - case EnrichedAssetEvent @event: - return await ResolveAssetTextAsync(@event.ToRef()); - } + var encoding = arguments.At(0).ToStringValue()?.ToUpperInvariant(); + var encoded = await asset.GetTextAsync(encoding, serviceProvider, default); - return ErrorNoAsset; + return new StringValue(encoded); }); options.Filters.AddFilter("assetBlurHash", async (input, arguments, context) => { - if (input is not ObjectValue objectValue) - { - return ErrorNoAsset; - } + TryGetAssetRef(input, out var asset); - async Task ResolveAssetHashAsync(AssetRef asset) - { - if (asset.FileSize > 512_000) - { - return ErrorTooBig; - } - - if (asset.Type != AssetType.Image) - { - return ErrorNoImage; - } + var options = new BlurOptions(); - var options = new BlurOptions(); + var arg0 = arguments.At(0); + var arg1 = arguments.At(1); - var arg0 = arguments.At(0); - var arg1 = arguments.At(1); + if (arg0.Type == FluidValues.Number) + { + options.ComponentX = (int)arg0.ToNumberValue(); + } - if (arg0.Type == FluidValues.Number) - { - options.ComponentX = (int)arg0.ToNumberValue(); - } + if (arg1.Type == FluidValues.Number) + { + options.ComponentX = (int)arg1.ToNumberValue(); + } - if (arg1.Type == FluidValues.Number) - { - options.ComponentX = (int)arg1.ToNumberValue(); - } + var blur = await asset.GetBlurHashAsync(options, serviceProvider, default); - var assetFileStore = serviceProvider.GetRequiredService(); - var assetThumbnailGenerator = serviceProvider.GetRequiredService(); + return new StringValue(blur); + }); + } - var blur = await asset.GetBlurHashAsync(options, assetFileStore, assetThumbnailGenerator, default); + private static bool TryGetAssetRef(FluidValue input, out AssetRef assetRef) + { + assetRef = default; - return new StringValue(blur); - } + if (input is not ObjectValue objectValue) + { + return false; + } - switch (objectValue.ToObjectValue()) - { - case IAssetEntity asset: - return await ResolveAssetHashAsync(asset.ToRef()); + switch (objectValue.ToObjectValue()) + { + case IAssetEntity asset: + assetRef = asset.ToRef(); + return true; - case EnrichedAssetEvent @event: - return await ResolveAssetHashAsync(@event.ToRef()); - } + case EnrichedAssetEvent @event: + assetRef = @event.ToRef(); + return true; + } - return ErrorNoAsset; - }); + return true; } private static async Task ResolveAssetAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs index 1a9f21ad1..b15373f28 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -8,6 +8,7 @@ using System.Security.Claims; using Jint; using Jint.Native; +using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Interop; using Microsoft.Extensions.DependencyInjection; @@ -15,17 +16,19 @@ using Squidex.Assets; 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.Apps; +using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Properties; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Assets; public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor { - private static readonly JsString ErrorNoAsset = new JsString(nameof(ErrorNoAsset)); - private static readonly JsString ErrorTooBig = new JsString(nameof(ErrorTooBig)); + private delegate void UpdateAssetDelegate(JsValue asset, JsValue metadata); private delegate void GetAssetsDelegate(JsValue references, Action callback); private delegate void GetAssetTextDelegate(JsValue asset, Action callback, JsValue? encoding); private delegate void GetBlurHashDelegate(JsValue asset, Action callback, JsValue? componentX, JsValue? componentY); @@ -41,16 +44,64 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor AddGetAssetText(context); AddGetAssetBlurHash(context); AddGetAssetObject(context); + AddUpdateAsset(context); + } + + private void AddUpdateAsset(ScriptExecutionContext context) + { + if (!context.TryGetValueIfExists("user", out var user)) + { + return; + } + + var updateAsset = new UpdateAssetDelegate((asset, metadata) => + { + UpdateAsset(context, user, asset, metadata); + }); + + context.Engine.SetValue("updateAsset", updateAsset); + } + + private void UpdateAsset(ScriptExecutionContext context, ClaimsPrincipal user, JsValue input, JsValue metadata) + { + context.Schedule(async (scheduler, ct) => + { + if (!TryGetAssetRef(context, input, out var asset) || metadata is not ObjectInstance metadataObj) + { + return; + } + + var commandBus = serviceProvider.GetRequiredService(); + + var assetMetadata = new AssetMetadata(); + + foreach (var (key, value) in metadataObj.GetOwnProperties()) + { + assetMetadata[key.AsString()] = JsonMapper.Map(value.Value); + } + + var command = new AnnotateAsset + { + FromRule = true, + AppId = asset.AppId, + Actor = RefToken.Client("Script"), + AssetId = asset.Id, + Metadata = assetMetadata, + User = user, + }; + + await commandBus.PublishAsync(command, default); + }); } private void AddGetAssetObject(ScriptExecutionContext context) { - if (!context.TryGetValue("appId", out var appId)) + if (!context.TryGetValueIfExists("appId", out var appId)) { return; } - if (!context.TryGetValue("user", out var user)) + if (!context.TryGetValueIfExists("user", out var user)) { return; } @@ -99,46 +150,16 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor context.Schedule(async (scheduler, ct) => { - if (input is not ObjectWrapper objectWrapper) + TryGetAssetRef(context, input, out var asset); + try { - scheduler.Run(callback, ErrorNoAsset); - return; - } + var text = await asset.GetTextAsync(encoding?.ToString(), serviceProvider, ct); - async Task ResolveAssetText(AssetRef asset) - { - if (asset.FileSize > 256_000) - { - scheduler.Run(callback, ErrorTooBig); - return; - } - - var assetFileStore = serviceProvider.GetRequiredService(); - try - { - var text = await asset.GetTextAsync(encoding?.ToString(), assetFileStore, ct); - - scheduler.Run(callback, text); - } - catch - { - scheduler.Run(callback, JsValue.Null); - } + scheduler.Run(callback, text); } - - switch (objectWrapper.Target) + catch { - case IAssetEntity asset: - await ResolveAssetText(asset.ToRef()); - break; - - case EnrichedAssetEvent e: - await ResolveAssetText(e.ToRef()); - break; - - default: - scheduler.Run(callback, ErrorNoAsset); - break; + scheduler.Run(callback, JsValue.Null); } }); } @@ -152,59 +173,29 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor context.Schedule(async (scheduler, ct) => { - if (input is not ObjectWrapper objectWrapper) + TryGetAssetRef(context, input, out var asset); + + var options = new BlurOptions(); + + if (componentX?.IsNumber() == true) { - scheduler.Run(callback, ErrorNoAsset); - return; + options.ComponentX = (int)componentX.AsNumber(); } - async Task ResolveHashAsync(AssetRef asset) + if (componentY?.IsNumber() == true) { - if (asset.FileSize > 512_000 || asset.Type != AssetType.Image) - { - scheduler.Run(callback, JsValue.Null); - return; - } - - var options = new BlurOptions(); - - if (componentX?.IsNumber() == true) - { - options.ComponentX = (int)componentX.AsNumber(); - } - - if (componentY?.IsNumber() == true) - { - options.ComponentX = (int)componentY.AsNumber(); - } - - var assetGenerator = serviceProvider.GetRequiredService(); - var assetFileStore = serviceProvider.GetRequiredService(); - try - { - var hash = await asset.GetBlurHashAsync(options, assetFileStore, assetGenerator, ct); - - scheduler.Run(callback, hash); - } - catch - { - scheduler.Run(callback, JsValue.Null); - } + options.ComponentX = (int)componentY.AsNumber(); } - switch (objectWrapper.Target) + try { - case IAssetEntity asset: - await ResolveHashAsync(asset.ToRef()); - break; - - case EnrichedAssetEvent @event: - await ResolveHashAsync(@event.ToRef()); - break; + var hash = await asset.GetBlurHashAsync(options, serviceProvider, ct); - default: - scheduler.Run(callback, ErrorNoAsset); - break; + scheduler.Run(callback, hash); + } + catch + { + scheduler.Run(callback, JsValue.Null); } }); } @@ -282,6 +273,49 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor }); } + private static bool TryGetAssetRef(ScriptExecutionContext context, JsValue input, out AssetRef assetRef) + { + assetRef = default; + + if (input is not ObjectWrapper objectWrapper) + { + return false; + } + + switch (objectWrapper.Target) + { + case IAssetEntity asset: + assetRef = asset.ToRef(); + return true; + + case EnrichedAssetEvent @event: + assetRef = @event.ToRef(); + return true; + + case AssetEntityScriptVars vars: + if (!context.TryGetValueIfExists(nameof(AssetScriptVars.AppName), out var appName) || + !context.TryGetValueIfExists(nameof(AssetScriptVars.AppId), out var appId) || + !context.TryGetValueIfExists(nameof(AssetScriptVars.AssetId), out var assetId)) + { + return false; + } + + context.TryGetValueIfExists(nameof(AssetScriptVars.FileId), out var fileId); + + assetRef = new AssetRef( + NamedId.Of(appId, appName), + assetId, + vars.GetValue(nameof(AssetEntityScriptVars.FileVersion)), + vars.GetValue(nameof(AssetEntityScriptVars.FileSize)), + vars.GetValue(nameof(AssetEntityScriptVars.MimeType)), + fileId, + vars.GetValue(nameof(AssetEntityScriptVars.Type))); + return true; + } + + return true; + } + private async Task GetAppAsync(DomainId appId, CancellationToken ct) { @@ -319,5 +353,8 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)", Resources.ScriptingGetBlurHash); + + describe(JsonType.Function, "updateAsset(asset, metadata)", + Resources.ScriptingUpdateAsset); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs index 50f8e0641..e4a983e46 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs @@ -21,4 +21,6 @@ public abstract class UploadAssetCommand : AssetCommand public AssetType Type { get; set; } public string FileHash { get; set; } + + public string FileId { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs index 43eb1e87c..4fec8c9a6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs @@ -88,6 +88,12 @@ public sealed class DefaultAssetFileStore : IAssetFileStore, IDeleter } } + public Task DownloadAsync(string tempFile, Stream stream, + CancellationToken ct = default) + { + return assetStore.DownloadAsync(tempFile, stream, default, ct); + } + public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, CancellationToken ct = default) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs index a52099eb7..5e943bdb9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs @@ -69,11 +69,12 @@ public sealed class AssetCommandMiddleware : CachingDomainObjectMiddleware -{ - private readonly AssetMetadata metadata; - - public int Count - { - get => metadata.Count; - } - - public ICollection Keys - { - get => metadata.Keys; - } - - public ICollection Values - { - get => metadata.Values.Cast().ToList(); - } - - public object? this[string key] - { - get => metadata[key]; - set => metadata[key] = JsonValue.Create(value); - } - - public bool IsReadOnly - { - get => false; - } - - public ScriptMetadataWrapper(AssetMetadata metadata) - { - this.metadata = metadata; - } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) - { - if (metadata.TryGetValue(key, out var temp)) - { - value = temp; - return true; - } - else - { - value = null; - return false; - } - } - - public void Add(string key, object? value) - { - metadata.Add(key, JsonValue.Create(value)); - } - - public void Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } - - public bool Remove(string key) - { - return metadata.Remove(key); - } - - public bool Remove(KeyValuePair item) - { - return false; - } - - public void Clear() - { - metadata.Clear(); - } - - public bool Contains(KeyValuePair item) - { - return false; - } - - public bool ContainsKey(string key) - { - return metadata.ContainsKey(key); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - var i = arrayIndex; - - foreach (var item in metadata) - { - if (i >= array.Length) - { - break; - } - - array[i] = new KeyValuePair(item.Key, item.Value); - i++; - } - } - - public IEnumerator> GetEnumerator() - { - return metadata.Select(x => new KeyValuePair(x.Key, x.Value)).GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)metadata).GetEnumerator(); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs index acf308a32..54ffca3f3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs @@ -36,6 +36,7 @@ public static class ScriptingExtensions // Script vars are just wrappers over dictionaries for better performance. var vars = new AssetScriptVars { + FileId = create.FileId, // Tags and metadata are mutable and can be changed from the scripts, but not replaced. Command = new AssetCommandScriptVars { @@ -52,7 +53,23 @@ public static class ScriptingExtensions Operation = "Create" }; - await ExecuteScriptAsync(operation, script, vars, ct); + var asset = new AssetEntityScriptVars + { + Type = create.Type, + FileHash = create.FileHash, + FileName = create.File.FileName, + FileSlug = create.File.FileName.Slugify(), + FileSize = create.File.FileSize, + FileVersion = 0, + IsProtected = false, + Metadata = create.Metadata, + MimeType = create.File.MimeType, + ParentId = create.ParentId, + ParentPath = await GetPathAsync(operation, create.ParentId, ct), + Tags = create.Tags + }; + + await ExecuteScriptAsync(operation, script, vars, asset, ct); } public static Task ExecuteUpdateScriptAsync(this AssetOperation operation, UpdateAsset update, @@ -68,6 +85,7 @@ public static class ScriptingExtensions // Script vars are just wrappers over dictionaries for better performance. var vars = new AssetScriptVars { + FileId = update.FileId, // Tags and metadata are mutable and can be changed from the scripts, but not replaced. Command = new AssetCommandScriptVars { @@ -81,7 +99,7 @@ public static class ScriptingExtensions Operation = "Update" }; - return ExecuteScriptAsync(operation, script, vars, ct); + return ExecuteScriptAsync(operation, script, vars, null, ct); } public static Task ExecuteAnnotateScriptAsync(this AssetOperation operation, AnnotateAsset annotate, @@ -109,7 +127,7 @@ public static class ScriptingExtensions Operation = "Annotate" }; - return ExecuteScriptAsync(operation, script, vars, ct); + return ExecuteScriptAsync(operation, script, vars, null, ct); } public static async Task ExecuteMoveScriptAsync(this AssetOperation operation, MoveAsset move, @@ -135,7 +153,7 @@ public static class ScriptingExtensions Operation = "Move" }; - await ExecuteScriptAsync(operation, script, vars, ct); + await ExecuteScriptAsync(operation, script, vars, null, ct); } public static Task ExecuteDeleteScriptAsync(this AssetOperation operation, DeleteAsset delete, @@ -158,30 +176,29 @@ public static class ScriptingExtensions Operation = "Delete" }; - return ExecuteScriptAsync(operation, script, vars, ct); + return ExecuteScriptAsync(operation, script, vars, null, ct); } - private static async Task ExecuteScriptAsync(AssetOperation operation, string script, AssetScriptVars vars, + private static async Task ExecuteScriptAsync(AssetOperation operation, string script, AssetScriptVars vars, AssetEntityScriptVars? asset, CancellationToken ct) { var snapshot = operation.Snapshot; - var parentPath = await GetPathAsync(operation, snapshot.ParentId, ct); - // Script vars are just wrappers over dictionaries for better performance. - var asset = new AssetEntityScriptVars + asset ??= new AssetEntityScriptVars { - Metadata = snapshot.Metadata, + Type = snapshot.Type, FileHash = snapshot.FileHash, FileName = snapshot.FileName, FileSize = snapshot.FileSize, FileSlug = snapshot.Slug, FileVersion = snapshot.FileVersion, IsProtected = snapshot.IsProtected, + Metadata = snapshot.Metadata, MimeType = snapshot.MimeType, ParentId = snapshot.ParentId, - ParentPath = parentPath, - Tags = snapshot.Tags + ParentPath = await GetPathAsync(operation, snapshot.ParentId, ct), + Tags = snapshot.Tags, }; vars.AppId = operation.App.Id; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs index 94110ce18..f2ad333d5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs @@ -26,6 +26,9 @@ public interface IAssetFileStore Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, CancellationToken ct = default); + Task DownloadAsync(string tempFile, Stream stream, + CancellationToken ct = default); + Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, CancellationToken ct = default); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index c9d45c97e..e678ed018 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -13,11 +13,11 @@ namespace Squidex.Domain.Apps.Entities.Assets; public sealed class ImageAssetMetadataSource : IAssetMetadataSource { - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IAssetThumbnailGenerator assetGenerator; - public ImageAssetMetadataSource(IAssetThumbnailGenerator assetThumbnailGenerator) + public ImageAssetMetadataSource(IAssetThumbnailGenerator assetGenerator) { - this.assetThumbnailGenerator = assetThumbnailGenerator; + this.assetGenerator = assetGenerator; } public async Task EnhanceAsync(UploadAssetCommand command, @@ -31,7 +31,7 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource await using (var uploadStream = command.File.OpenRead()) { - imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, mimeType, ct); + imageInfo = await assetGenerator.GetImageInfoAsync(uploadStream, mimeType, ct); } if (imageInfo != null) @@ -48,13 +48,13 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource { await using (var tempStream = tempFile.OpenWrite()) { - await assetThumbnailGenerator.FixAsync(uploadStream, mimeType, tempStream, ct); + await assetGenerator.FixAsync(uploadStream, mimeType, tempStream, ct); } } await using (var tempStream = tempFile.OpenRead()) { - imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(tempStream, mimeType, ct) ?? imageInfo; + imageInfo = await assetGenerator.GetImageInfoAsync(tempStream, mimeType, ct) ?? imageInfo; } await command.File.DisposeAsync(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/ScriptAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/ScriptAsset.cs index b0da1192d..8ca83d37c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/ScriptAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/ScriptAsset.cs @@ -69,13 +69,14 @@ public sealed class ScriptAsset : IAssetEnricherStep AssetId = asset.Id, Asset = new AssetEntityScriptVars { - Metadata = asset.Metadata, + Type = asset.Type, FileHash = asset.FileHash, FileName = asset.FileName, FileSize = asset.FileSize, FileSlug = asset.Slug, FileVersion = asset.FileVersion, IsProtected = asset.IsProtected, + Metadata = asset.Metadata, MimeType = asset.MimeType, ParentId = asset.ParentId, ParentPath = null, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs index 4930e8f8e..c483a7e61 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs @@ -6,6 +6,8 @@ // ========================================================================== using System.Text; +using Fluid.Values; +using Microsoft.Extensions.DependencyInjection; using Squidex.Assets; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; @@ -18,46 +20,62 @@ using Squidex.Infrastructure.ObjectPool; namespace Squidex.Domain.Apps.Entities.Assets; public record struct AssetRef( - DomainId AppId, + NamedId AppId, DomainId Id, long FileVersion, long FileSize, string MimeType, + string? FileId, AssetType Type); public static class Transformations { + private const int MaxSize = 4 * 1024 * 1024; + private const string ErrorNoAsset = "NoAsset"; + private const string ErrorTooBig = "ErrorTooBig"; + public static AssetRef ToRef(this EnrichedAssetEvent @event) { return new AssetRef( - @event.AppId.Id, + @event.AppId, @event.Id, @event.FileVersion, @event.FileSize, @event.MimeType, + null, @event.AssetType); } public static AssetRef ToRef(this IAssetEntity asset) { return new AssetRef( - asset.AppId.Id, + asset.AppId, asset.Id, asset.FileVersion, asset.FileSize, asset.MimeType, + null, asset.Type); } - public static async Task GetTextAsync(this AssetRef asset, string? encoding, - IAssetFileStore assetFileStore, + public static async Task GetTextAsync(this AssetRef asset, string? encoding, IServiceProvider services, CancellationToken ct = default) { - using (var stream = DefaultPools.MemoryStream.GetStream()) + if (asset == default) + { + return ErrorNoAsset; + } + + if (asset.FileSize > MaxSize) { - await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct); + return ErrorTooBig; + } - stream.Position = 0; + var assetFileStore = services.GetRequiredService(); + + using (var stream = DefaultPools.MemoryStream.GetStream()) + { + await DownloadAsync(asset, assetFileStore, stream, ct); var bytes = stream.ToArray(); @@ -75,18 +93,41 @@ public static class Transformations } } - public static async Task GetBlurHashAsync(this AssetRef asset, BlurOptions options, - IAssetFileStore assetFileStore, - IAssetThumbnailGenerator assetThumbnails, + public static async Task GetBlurHashAsync(this AssetRef asset, BlurOptions options, IServiceProvider services, CancellationToken ct = default) { + if (asset == default) + { + return ErrorNoAsset; + } + + if (asset.FileSize > MaxSize || asset.Type != AssetType.Image) + { + return null; + } + + var assetFileStore = services.GetRequiredService(); + var assetGenerator = services.GetRequiredService(); + using (var stream = DefaultPools.MemoryStream.GetStream()) { - await assetFileStore.DownloadAsync(asset.AppId, asset.Id, asset.FileVersion, null, stream, default, ct); + await DownloadAsync(asset, assetFileStore, stream, ct); - stream.Position = 0; + return await assetGenerator.ComputeBlurHashAsync(stream, asset.MimeType, options, ct); + } + } - return await assetThumbnails.ComputeBlurHashAsync(stream, asset.MimeType, options, ct); + private static async Task DownloadAsync(AssetRef asset, IAssetFileStore assetFileStore, MemoryStream stream, CancellationToken ct) + { + if (asset.FileId != null) + { + await assetFileStore.DownloadAsync(asset.FileId, stream, ct); } + else + { + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, stream, default, ct); + } + + stream.Position = 0; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index 94886db15..89f8c0ce3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs @@ -26,7 +26,7 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor public void Extend(ScriptExecutionContext context) { - if (!context.TryGetValue("appId", out var appId)) + if (!context.TryGetValueIfExists("appId", out var appId)) { return; } @@ -48,7 +48,7 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor public void ExtendAsync(ScriptExecutionContext context) { - if (!context.TryGetValue("appId", out var appId)) + if (!context.TryGetValueIfExists("appId", out var appId)) { return; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingBatchLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingBatchLoader.cs new file mode 100644 index 000000000..a8f4068f7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingBatchLoader.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.DataLoader; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; + +#pragma warning disable MA0048 // File name must match type name +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter +#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Cache; + +record struct CacheableId(T Id, TimeSpan CacheDuration = default); + +internal class CachingBatchDataLoader : DataLoaderBase, T> where TKey : notnull where T : class +{ + private readonly IQueryCache queryCache; + private readonly Func, CancellationToken, Task>> queryDelegate; + + public CachingBatchDataLoader(IQueryCache queryStore, + Func, CancellationToken, Task>> queryDelegate, bool canCache = true, int maxBatchSize = int.MaxValue) + : base(canCache, maxBatchSize) + { + this.queryCache = queryStore; + this.queryDelegate = queryDelegate; + } + + protected override async Task FetchAsync(IEnumerable, T>> list, + CancellationToken cancellationToken) + { + var unmatched = new List, T>>(list.Count()); + + foreach (var entry in list) + { + if (entry.Key.CacheDuration != default && queryCache.TryGet(entry.Key.Id, out var cached)) + { + entry.SetResult(cached); + } + else + { + unmatched.Add(entry); + } + } + + if (unmatched.Count == 0) + { + return; + } + + var ids = unmatched.Select(x => x.Key.Id).Distinct(); + + var entries = await queryDelegate(ids, cancellationToken); + + foreach (var entry in unmatched) + { + entries.TryGetValue(entry.Key.Id, out var value); + entry.SetResult(value!); + + if (value != null && entry.Key.CacheDuration != default) + { + queryCache.Set(entry.Key.Id, value, entry.Key.CacheDuration); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingDataLoaderExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingDataLoaderExtensions.cs new file mode 100644 index 000000000..95de4f527 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingDataLoaderExtensions.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.DataLoader; +using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Translations; +using TagLib.IFD.Tags; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Cache; + +internal static class CachingDataLoaderExtensions +{ + public static IDataLoader, T> GetOrAddCachingLoader(this DataLoaderContext dataLoaderContext, IQueryCache queryCache, string loaderKey, + Func, CancellationToken, Task>> queryDelegate, bool canCache = true, int maxBatchSize = int.MaxValue) + where TKey : notnull where T : class + { + return dataLoaderContext.GetOrAdd(loaderKey, () => + { + return new CachingBatchDataLoader(queryCache, queryDelegate, canCache, maxBatchSize); + }); + } + + public static IDataLoader GetOrAddNonCachingBatchLoader(this DataLoaderContext dataLoaderContext, string loaderKey, + Func, CancellationToken, Task>> queryDelegate, int maxBatchSize = int.MaxValue) + where TKey : notnull where T : class + { + return dataLoaderContext.GetOrAdd(loaderKey, () => + { + return new NonCachingBatchLoader(queryDelegate, maxBatchSize); + }); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/EmptyDataLoaderResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/EmptyDataLoaderResult.cs new file mode 100644 index 000000000..4ba44fc88 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/EmptyDataLoaderResult.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.DataLoader; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Cache; + +public sealed class EmptyDataLoaderResult : IDataLoaderResult +{ + public Task GetResultAsync( + CancellationToken cancellationToken = default) + { + return Task.FromResult(Array.Empty()); + } + + Task IDataLoaderResult.GetResultAsync( + CancellationToken cancellationToken) + { + return Task.FromResult(Array.Empty()); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/NonCachingBatchLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/NonCachingBatchLoader.cs new file mode 100644 index 000000000..4e89a9714 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/NonCachingBatchLoader.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.DataLoader; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Cache; + +internal class NonCachingBatchLoader : DataLoaderBase where TKey : notnull where T : class +{ + private readonly Func, CancellationToken, Task>> queryDelegate; + + public NonCachingBatchLoader(Func, CancellationToken, Task>> queryDelegate, int maxBatchSize = int.MaxValue) + : base(false, maxBatchSize) + { + this.queryDelegate = queryDelegate; + } + + protected override async Task FetchAsync(IEnumerable> list, + CancellationToken cancellationToken) + { + var dictionary = await queryDelegate(list.Select(x => x.Key), cancellationToken).ConfigureAwait(false); + + foreach (var item in list) + { + dictionary.TryGetValue(item.Key, out var value); + + item.SetResult(value!); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index f6b7517bd..293e22153 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -6,20 +6,21 @@ // ========================================================================== using GraphQL.DataLoader; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Cache; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Infrastructure; using Squidex.Shared.Users; -#pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections - namespace Squidex.Domain.Apps.Entities.Contents.GraphQL; public sealed class GraphQLExecutionContext : QueryExecutionContext { - private static readonly List EmptyAssets = new List(); - private static readonly List EmptyContents = new List(); + private static readonly EmptyDataLoaderResult EmptyAssets = new EmptyDataLoaderResult(); + private static readonly EmptyDataLoaderResult EmptyContents = new EmptyDataLoaderResult(); private readonly IDataLoaderContextAccessor dataLoaders; + private readonly GraphQLOptions options; public override Context Context { get; } @@ -30,7 +31,8 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext IContentQueryService contentQuery, IContentCache contentCache, IServiceProvider serviceProvider, - Context context) + Context context, + IOptions options) : base(assetQuery, assetCache, contentQuery, contentCache, serviceProvider) { this.dataLoaders = dataLoaders; @@ -39,6 +41,8 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext .WithoutCleanup() .WithoutContentEnrichment() .WithoutAssetEnrichment()); + + this.options = options.Value; } public async ValueTask FindUserAsync(RefToken refToken, @@ -56,104 +60,84 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext } } - public async Task GetAssetAsync(DomainId id, TimeSpan cacheDuration, - CancellationToken ct) + public IDataLoaderResult GetContent(DomainId schemaId, DomainId id, long version) { - var assets = await GetAssetsAsync(new List { id }, cacheDuration, ct); - var asset = assets.FirstOrDefault(); + return dataLoaders.Context!.GetOrAddLoader(nameof(GetContent), ct => + { + return FindContentAsync(schemaId.ToString(), id, version, ct); + }).LoadAsync(); + } + + public IDataLoaderResult GetAsset(DomainId id, + TimeSpan cacheDuration) + { + var assets = GetAssets(new List { id }, cacheDuration); + var asset = assets.Then(x => x.FirstOrDefault()); return asset; } - public async Task GetContentAsync(DomainId schemaId, DomainId id, HashSet? fields, TimeSpan cacheDuration, - CancellationToken ct) + public IDataLoaderResult GetContent(DomainId schemaId, DomainId id, HashSet? fields, + TimeSpan cacheDuration) { - var contents = await GetContentsAsync(new List { id }, fields, cacheDuration, ct); - var content = contents.FirstOrDefault(x => x.SchemaId.Id == schemaId); + var contents = GetContents(new List { id }, fields, cacheDuration); + var content = contents.Then(x => x.FirstOrDefault(x => x.SchemaId.Id == schemaId)); return content; } - public async Task> GetAssetsAsync(List? ids, TimeSpan cacheDuration, - CancellationToken ct) + public IDataLoaderResult GetAssets(List? ids, + TimeSpan cacheDuration) { if (ids == null || ids.Count == 0) { return EmptyAssets; } - async Task> LoadAsync(IEnumerable ids) - { - var result = await GetAssetsLoader().LoadAsync(ids).GetResultAsync(ct); - - return result?.NotNull().ToList() ?? EmptyAssets; - } - - if (cacheDuration > TimeSpan.Zero) - { - var assets = await AssetCache.CacheOrQueryAsync(ids, async pendingIds => - { - return await LoadAsync(pendingIds); - }, cacheDuration); - - return assets; - } - - return await LoadAsync(ids); + return GetAssetsLoader().LoadAsync(BuildKeys(ids, cacheDuration)).Then(x => x.NotNull().ToArray()); } - public async Task> GetContentsAsync(List? ids, HashSet? fields, TimeSpan cacheDuration, - CancellationToken ct) + public IDataLoaderResult GetContents(List? ids, HashSet? fields, + TimeSpan cacheDuration) { if (ids == null || ids.Count == 0) { return EmptyContents; } - if (cacheDuration > TimeSpan.Zero || fields == null) + if (fields == null) { - var contents = await ContentCache.CacheOrQueryAsync(ids, async pendingIds => - { - var result = await GetContentsLoader().LoadAsync(ids).GetResultAsync(ct); - - return result?.NotNull().ToList() ?? EmptyContents; - }, cacheDuration); - - return contents.ToList(); + return GetContentsLoader().LoadAsync(BuildKeys(ids, cacheDuration)).Then(x => x.NotNull().ToArray()); } - else - { - var contents = await GetContentsLoaderWithFields().LoadAsync(ids.Select(x => (x, fields))).GetResultAsync(ct); - return contents?.NotNull().ToList() ?? EmptyContents; - } + return GetContentsLoaderWithFields().LoadAsync(BuildKeys(ids, fields)).Then(x => x.NotNull().ToArray()); } - private IDataLoader GetAssetsLoader() + private IDataLoader, IEnrichedAssetEntity> GetAssetsLoader() { - return dataLoaders.Context!.GetOrAddBatchLoader(nameof(GetAssetsLoader), + return dataLoaders.Context!.GetOrAddCachingLoader(AssetCache, nameof(GetAssetsLoader), async (batch, ct) => { - var result = await QueryAssetsByIdsAsync(new List(batch), ct); + var result = await QueryAssetsByIdsAsync(batch, ct); return result.ToDictionary(x => x.Id); - }); + }, maxBatchSize: options.DataLoaderBatchSize); } - private IDataLoader GetContentsLoader() + private IDataLoader, IEnrichedContentEntity> GetContentsLoader() { - return dataLoaders.Context!.GetOrAddBatchLoader(nameof(GetContentsLoader), + return dataLoaders.Context!.GetOrAddCachingLoader(ContentCache, nameof(GetContentsLoader), async (batch, ct) => { var result = await QueryContentsByIdsAsync(batch, null, ct); return result.ToDictionary(x => x.Id); - }); + }, maxBatchSize: options.DataLoaderBatchSize); } private IDataLoader<(DomainId Id, HashSet Fields), IEnrichedContentEntity> GetContentsLoaderWithFields() { - return dataLoaders.Context!.GetOrAddBatchLoader<(DomainId Id, HashSet Fields), IEnrichedContentEntity>(nameof(GetContentsLoader), + return dataLoaders.Context!.GetOrAddNonCachingBatchLoader<(DomainId Id, HashSet Fields), IEnrichedContentEntity>(nameof(GetContentsLoaderWithFields), async (batch, ct) => { var fields = batch.SelectMany(x => x.Fields).ToHashSet(); @@ -161,7 +145,7 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext var result = await QueryContentsByIdsAsync(batch.Select(x => x.Id), fields, ct); return result.ToDictionary(x => (x.Id, fields)); - }); + }, maxBatchSize: options.DataLoaderBatchSize); } private IDataLoader GetUserLoader() @@ -174,4 +158,30 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext return result; }); } + + private static (DomainId, HashSet)[] BuildKeys(List ids, HashSet fields) + { + // Use manual loops and arrays to avoid allocations. + var keys = new (DomainId, HashSet)[ids.Count]; + + for (var i = 0; i < ids.Count; i++) + { + keys[i] = (ids[0], fields); + } + + return keys; + } + + private static CacheableId[] BuildKeys(List ids, TimeSpan cacheDuration) + { + // Use manual loops and arrays to avoid allocations. + var keys = new CacheableId[ids.Count]; + + for (var i = 0; i < ids.Count; i++) + { + keys[i] = new CacheableId(ids[i], cacheDuration); + } + + return keys; + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs index 566f88ea3..7babdaf31 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs @@ -11,5 +11,7 @@ public sealed class GraphQLOptions { public int CacheDuration { get; set; } = 10 * 60; + public int DataLoaderBatchSize { get; set; } = 1000; + public bool EnableSubscriptions { get; set; } = true; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs index 5dfa5ce83..28d1177a8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs @@ -57,13 +57,12 @@ internal static class AssetActions } }; - public static readonly IFieldResolver Resolver = Resolvers.Async(async (_, fieldContext, context) => + public static readonly IFieldResolver Resolver = Resolvers.Sync((_, fieldContext, context) => { var assetId = fieldContext.GetArgument("id"); - return await context.GetAssetAsync(assetId, - fieldContext.CacheDuration(), - fieldContext.CancellationToken); + return context.GetAsset(assetId, + fieldContext.CacheDuration()); }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index 5b24d5041..e9892bac9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -81,7 +81,7 @@ internal static class ContentActions } }; - public static readonly IFieldResolver Resolver = Resolvers.Async(async (_, fieldContext, context) => + public static readonly IFieldResolver Resolver = Resolvers.Sync((_, fieldContext, context) => { var contentId = fieldContext.GetArgument("id"); @@ -90,15 +90,13 @@ internal static class ContentActions if (contentVersion >= 0) { - return await context.FindContentAsync(contentSchemaId.ToString(), contentId, contentVersion.Value, - fieldContext.CancellationToken); + return context.GetContent(contentSchemaId, contentId, contentVersion.Value); } else { - return await context.GetContentAsync(contentSchemaId, contentId, + return context.GetContent(contentSchemaId, contentId, fieldContext.FieldNames(), - fieldContext.CacheDuration(), - fieldContext.CancellationToken); + fieldContext.CacheDuration()); } }); } @@ -115,14 +113,13 @@ internal static class ContentActions } }; - public static readonly IFieldResolver Resolver = Resolvers.Async(async (_, fieldContext, context) => + public static readonly IFieldResolver Resolver = Resolvers.Sync((_, fieldContext, context) => { var ids = fieldContext.GetArgument("ids").ToList(); - return await context.GetContentsAsync(ids, + return context.GetContents(ids, fieldContext.FieldNames(), - fieldContext.CacheDuration(), - fieldContext.CancellationToken); + fieldContext.CacheDuration()); }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs index 41c5adc3a..284657819 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs @@ -19,23 +19,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; internal static class ContentFields { - public static readonly IFieldResolver ResolveStringFieldAssets = Resolvers.Async(async (value, fieldContext, context) => + public static readonly IFieldResolver ResolveStringFieldAssets = Resolvers.Sync((value, fieldContext, context) => { var ids = context.Resolve().GetEmbeddedAssetIds(value).ToList(); - return await context.GetAssetsAsync(ids, - fieldContext.CacheDuration(), - fieldContext.CancellationToken); + return context.GetAssets(ids, + fieldContext.CacheDuration()); }); - public static readonly IFieldResolver ResolveStringFieldContents = Resolvers.Async(async (value, fieldContext, context) => + public static readonly IFieldResolver ResolveStringFieldContents = Resolvers.Sync((value, fieldContext, context) => { var ids = context.Resolve().GetEmbeddedContentIds(value).ToList(); - return await context.GetContentsAsync(ids, + return context.GetContents(ids, fieldContext.FieldNames(), - fieldContext.CacheDuration(), - fieldContext.CancellationToken); + fieldContext.CacheDuration()); }); public static readonly FieldType Id = new FieldType diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs index 93189e8b8..42aea39af 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs @@ -98,23 +98,21 @@ internal sealed class FieldVisitor : IFieldVisitor } }); - private static readonly IFieldResolver Assets = CreateAsyncValueResolver((value, fieldContext, context) => + private static readonly IFieldResolver Assets = CreateValueResolver((value, fieldContext, context) => { var ids = value.AsIds(); - return context.GetAssetsAsync(ids, - fieldContext.CacheDuration(), - fieldContext.CancellationToken); + return context.GetAssets(ids, + fieldContext.CacheDuration()); }); - private static readonly IFieldResolver References = CreateAsyncValueResolver((value, fieldContext, context) => + private static readonly IFieldResolver References = CreateValueResolver((value, fieldContext, context) => { var ids = value.AsIds(); - return context.GetContentsAsync(ids, + return context.GetContents(ids, fieldContext.FieldNames(), - fieldContext.CacheDuration(), - fieldContext.CancellationToken); + fieldContext.CacheDuration()); }); private readonly Builder builder; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 05288a1ff..b39713a43 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -221,7 +221,7 @@ public sealed class ContentQueryService : IContentQueryService var canCache = !context.IsFrontendClient; - if (Guid.TryParse(schemaIdOrName, out var guid)) + if (Guid.TryParseExact(schemaIdOrName, "D", out var guid)) { var schemaId = DomainId.Create(guid); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 07bd7b1c9..4f53aa40d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -65,8 +65,6 @@ public abstract class QueryExecutionContext : Dictionary maxRequests.Release(); } - AssetCache.SetMany(assets.Select(x => (x.Id, x))!); - return assets; } @@ -85,8 +83,6 @@ public abstract class QueryExecutionContext : Dictionary maxRequests.Release(); } - ContentCache.SetMany(contents.Select(x => (x.Id, x))!); - return contents; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs index 20c056401..4c65ab1fa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs @@ -29,12 +29,12 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor public void ExtendAsync(ScriptExecutionContext context) { - if (!context.TryGetValue("appId", out var appId)) + if (!context.TryGetValueIfExists("appId", out var appId)) { return; } - if (!context.TryGetValue("user", out var user)) + if (!context.TryGetValueIfExists("user", out var user)) { return; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs index 65a769a4a..43d445b07 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs @@ -185,5 +185,14 @@ namespace Squidex.Domain.Apps.Entities.Properties { return ResourceManager.GetString("ScriptingResetCounterV2", resourceCulture); } } + + /// + /// Looks up a localized string similar to Update the metadata of the asset.. + /// + internal static string ScriptingUpdateAsset { + get { + return ResourceManager.GetString("ScriptingUpdateAsset", resourceCulture); + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx index 26e5d970d..436d4ae6d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx +++ b/backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx @@ -159,4 +159,7 @@ Resets the counter with the given name to zero. + + Update the metadata of the asset. + \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 72127c150..0f04dda77 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable en enable diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index 4cb2d5a7b..cb1e9dcf6 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index 68da5334a..90c9fa556 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 677f6f044..81e3fe785 100644 --- a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj index a66abd4df..d2b7e2c3d 100644 --- a/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj +++ b/backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj @@ -2,7 +2,7 @@ net7.0 Squidex.Infrastructure - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj index 8c545d629..96cf9374d 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -2,7 +2,7 @@ net7.0 Squidex.Infrastructure - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs index ebf2977dd..7239513c7 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs @@ -143,10 +143,9 @@ public sealed class MongoEventStoreSubscription : IEventSubscription if (byStream != null) { var filterBuilder = Builders>.Filter; + var filterExpression = filterBuilder.Or(filterBuilder.Ne(x => x.OperationType, ChangeStreamOperationType.Insert), byStream); - var filter = filterBuilder.Or(filterBuilder.Ne(x => x.OperationType, ChangeStreamOperationType.Insert), byStream); - - return result.Match(filter); + return result.Match(filterExpression); } return result; diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index 54a2d64ec..adaa68d6e 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -166,12 +166,12 @@ public partial class MongoEventStore : MongoRepositoryBase, IE var filterDefinition = CreateFilter(streamFilter, lastPosition); var find = - Collection.Find(filterDefinition) + Collection.Find(filterDefinition).SortBy(x => x.Timestamp).ThenByDescending(x => x.EventStream) .Limit(take); var taken = 0; - await foreach (var current in find.ToAsyncEnumerable(ct).OrderBy(x => x.Timestamp).ThenBy(x => x.EventStream)) + await foreach (var current in find.ToAsyncEnumerable(ct)) { foreach (var @event in current.Filtered(lastPosition)) { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs index 9f25c0141..d094227b9 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs @@ -81,6 +81,8 @@ public sealed class MongoRequestLogRepository : MongoRepositoryBase x.Key == key && x.Timestamp >= timestampStart && x.Timestamp < timestampEnd); - return find.ToAsyncEnumerable(ct).Select(x => x.ToRequest()); + var documents = find.ToAsyncEnumerable(ct); + + return documents.Select(x => x.ToRequest()); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index ede65dc22..c1fb92369 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -57,7 +57,7 @@ public static class MongoExtensions public static async IAsyncEnumerable ToAsyncEnumerable(this IFindFluent find, [EnumeratorCancellation] CancellationToken ct = default) { - var cursor = await find.ToCursorAsync(ct); + using var cursor = await find.ToCursorAsync(ct); while (await cursor.MoveNextAsync(ct)) { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 1de5a4f9a..b12f9044e 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -2,7 +2,7 @@ net7.0 Squidex.Infrastructure - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs b/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs index 98dcbea0b..0b693f8c2 100644 --- a/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs +++ b/backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs @@ -7,11 +7,9 @@ namespace Squidex.Infrastructure.Caching; -public interface IQueryCache where TKey : notnull where T : class, IWithId +public interface IQueryCache where TKey : notnull { - void SetMany(IEnumerable<(TKey, T?)> results, - TimeSpan? permanentDuration = null); + void Set(TKey key, T item, TimeSpan cacheDuration); - Task> CacheOrQueryAsync(IEnumerable keys, Func, Task>> query, - TimeSpan? permanentDuration = null); + bool TryGet(TKey key, out T result); } diff --git a/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs b/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs index e8d94cfce..a1bccc4b8 100644 --- a/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs +++ b/backend/src/Squidex.Infrastructure/Caching/QueryCache.cs @@ -5,89 +5,46 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Concurrent; using Microsoft.Extensions.Caching.Memory; namespace Squidex.Infrastructure.Caching; -public class QueryCache : IQueryCache where TKey : notnull where T : class, IWithId +public class QueryCache : IQueryCache where TKey : notnull { - private readonly ConcurrentDictionary entries = new ConcurrentDictionary(); - private readonly IMemoryCache? memoryCache; + private readonly IMemoryCache? cacheStore; + private readonly string? cacheKeyPrefix; - public QueryCache(IMemoryCache? memoryCache = null) + public QueryCache(IMemoryCache? cacheStore = null, string? cacheKeyPrefix = null) { - this.memoryCache = memoryCache; + this.cacheStore = cacheStore; + this.cacheKeyPrefix = cacheKeyPrefix; } - public void SetMany(IEnumerable<(TKey, T?)> results, - TimeSpan? permanentDuration = null) + public void Set(TKey key, T item, TimeSpan cacheDuration) { - Guard.NotNull(results); - - foreach (var (key, value) in results) + if (cacheStore == null) { - Set(key, value, permanentDuration); + return; } - } - - private void Set(TKey key, T? value, - TimeSpan? permanentDuration = null) - { - entries[key] = value; - if (memoryCache != null && permanentDuration > TimeSpan.Zero) - { - memoryCache.Set(key, value, permanentDuration.Value); - } + cacheStore.Set((cacheKeyPrefix, key), item, cacheDuration); } - public async Task> CacheOrQueryAsync(IEnumerable keys, Func, Task>> query, - TimeSpan? permanentDuration = null) + public bool TryGet(TKey key, out T result) { - Guard.NotNull(keys); - Guard.NotNull(query); - - var items = GetMany(keys, permanentDuration.HasValue); - - var pendingIds = new HashSet(keys.Where(key => !items.ContainsKey(key))); + result = default!; - if (pendingIds.Count > 0) + if (cacheStore == null) { - var queried = (await query(pendingIds)).ToDictionary(x => x.Id); - - foreach (var id in pendingIds) - { - queried.TryGetValue(id, out var item); - - items[id] = item; - - Set(id, item, permanentDuration); - } + return false; } - return items.Values.NotNull().ToList(); - } - - private Dictionary GetMany(IEnumerable keys, - bool fromPermanentCache = false) - { - var result = new Dictionary(); - - foreach (var key in keys) + if (cacheStore.TryGetValue((cacheKeyPrefix, key), out var item) && item is T typed) { - if (entries.TryGetValue(key, out var value)) - { - result[key] = value; - } - else if (fromPermanentCache && memoryCache != null && memoryCache.TryGetValue(key, out value)) - { - result[key] = value; - - entries[key] = value; - } + result = typed; + return true; } - return result; + return false; } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs deleted file mode 100644 index 4bece008d..000000000 --- a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs +++ /dev/null @@ -1,125 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections; - -namespace Squidex.Infrastructure.Collections; - -public partial class ListDictionary -{ - private sealed class KeyCollection : ICollection - { - private readonly ListDictionary dictionary; - - public int Count - { - get => dictionary.Count; - } - - public bool IsReadOnly - { - get => false; - } - - public KeyCollection(ListDictionary dictionary) - { - this.dictionary = dictionary; - } - - public void Add(TKey item) - { - throw new NotSupportedException(); - } - - public void Clear() - { - throw new NotSupportedException(); - } - - public void CopyTo(TKey[] array, int arrayIndex) - { - var i = 0; - foreach (var (key, _) in dictionary.entries) - { - array[arrayIndex + i] = key; - i++; - } - } - - public bool Remove(TKey item) - { - throw new NotSupportedException(); - } - - public bool Contains(TKey item) - { - foreach (var entry in dictionary.entries) - { - if (dictionary.comparer.Equals(entry.Key, item)) - { - return true; - } - } - - return false; - } - - public IEnumerator GetEnumerator() - { - return new Enumerator(dictionary); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(dictionary); - } - - private struct Enumerator : IEnumerator, IEnumerator - { - private readonly ListDictionary dictionary; - private int index = -1; - private TKey value = default!; - - readonly TKey IEnumerator.Current - { - get => value!; - } - - readonly object IEnumerator.Current - { - get => value!; - } - - public Enumerator(ListDictionary dictionary) - { - this.dictionary = dictionary; - } - - public readonly void Dispose() - { - } - - public bool MoveNext() - { - if (index >= dictionary.entries.Count - 1) - { - return false; - } - - index++; - - value = dictionary.entries[index].Key; - return true; - } - - public void Reset() - { - index = -1; - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs deleted file mode 100644 index 4f9987068..000000000 --- a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs +++ /dev/null @@ -1,125 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections; - -namespace Squidex.Infrastructure.Collections; - -public partial class ListDictionary -{ - private sealed class ValueCollection : ICollection - { - private readonly ListDictionary dictionary; - - public int Count - { - get => dictionary.Count; - } - - public bool IsReadOnly - { - get => false; - } - - public ValueCollection(ListDictionary dictionary) - { - this.dictionary = dictionary; - } - - public void Add(TValue item) - { - throw new NotSupportedException(); - } - - public void Clear() - { - throw new NotSupportedException(); - } - - public void CopyTo(TValue[] array, int arrayIndex) - { - var i = 0; - foreach (var (_, value) in dictionary.entries) - { - array[arrayIndex + i] = value; - i++; - } - } - - public bool Remove(TValue item) - { - throw new NotSupportedException(); - } - - public bool Contains(TValue item) - { - foreach (var entry in dictionary.entries) - { - if (Equals(entry.Value, item)) - { - return true; - } - } - - return false; - } - - public IEnumerator GetEnumerator() - { - return new Enumerator(dictionary); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(dictionary); - } - - private struct Enumerator : IEnumerator, IEnumerator - { - private readonly ListDictionary dictionary; - private int index = -1; - private TValue value = default!; - - readonly TValue IEnumerator.Current - { - get => value!; - } - - readonly object IEnumerator.Current - { - get => value!; - } - - public Enumerator(ListDictionary dictionary) - { - this.dictionary = dictionary; - } - - public readonly void Dispose() - { - } - - public bool MoveNext() - { - if (index >= dictionary.entries.Count - 1) - { - return false; - } - - index++; - - value = dictionary.entries[index].Value; - return true; - } - - public void Reset() - { - index = -1; - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs deleted file mode 100644 index a8af28348..000000000 --- a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs +++ /dev/null @@ -1,273 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections; -using System.Diagnostics.CodeAnalysis; - -namespace Squidex.Infrastructure.Collections; - -public partial class ListDictionary : IDictionary, IReadOnlyDictionary where TKey : notnull -{ - private readonly List> entries = new List>(); - private readonly IEqualityComparer comparer; - - private struct Enumerator : IEnumerator>, IEnumerator - { - private readonly ListDictionary dictionary; - private int index = -1; - private KeyValuePair value = default!; - - readonly KeyValuePair IEnumerator>.Current - { - get => value!; - } - - readonly object IEnumerator.Current - { - get => value!; - } - - public Enumerator(ListDictionary dictionary) - { - this.dictionary = dictionary; - } - - public readonly void Dispose() - { - } - - public bool MoveNext() - { - if (index >= dictionary.entries.Count - 1) - { - return false; - } - - index++; - - value = dictionary.entries[index]; - return true; - } - - public void Reset() - { - index = -1; - } - } - - public TValue this[TKey key] - { - get - { - if (!TryGetValue(key, out var result)) - { - ThrowHelper.KeyNotFoundException(); - return default!; - } - - return result; - } - set - { - var index = -1; - - for (var i = 0; i < entries.Count; i++) - { - if (comparer.Equals(entries[i].Key, key)) - { - index = i; - break; - } - } - - if (index >= 0) - { - entries[index] = new KeyValuePair(key, value); - } - else - { - entries.Add(new KeyValuePair(key, value)); - } - } - } - - public ICollection Keys - { - get => new KeyCollection(this); - } - - public ICollection Values - { - get => new ValueCollection(this); - } - - public int Count - { - get => entries.Count; - } - - public int Capacity - { - get => entries.Capacity; - } - - public bool IsReadOnly - { - get => false; - } - - IEnumerable IReadOnlyDictionary.Keys - { - get => new KeyCollection(this); - } - - IEnumerable IReadOnlyDictionary.Values - { - get => new ValueCollection(this); - } - - public ListDictionary() - : this(1, null) - { - } - - public ListDictionary(ListDictionary source, IEqualityComparer? comparer = null) - { - Guard.NotNull(source); - - entries = source.entries.ToList(); - - this.comparer = comparer ?? EqualityComparer.Default; - } - - public ListDictionary(int capacity, IEqualityComparer? comparer = null) - { - Guard.GreaterEquals(capacity, 0); - - entries = new List>(capacity); - - this.comparer = comparer ?? EqualityComparer.Default; - } - - public void Add(TKey key, TValue value) - { - if (ContainsKey(key)) - { - ThrowHelper.ArgumentException("Key already exists.", nameof(key)); - } - - AddUnsafe(key, value); - } - - public void Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } - - public void AddUnsafe(TKey key, TValue value) - { - entries.Add(new KeyValuePair(key, value)); - } - - public void Clear() - { - entries.Clear(); - } - - public bool Contains(KeyValuePair item) - { - foreach (var entry in entries) - { - if (comparer.Equals(entry.Key, item.Key) && Equals(entry.Value, item.Value)) - { - return true; - } - } - - return false; - } - - public bool ContainsKey(TKey key) - { - foreach (var entry in entries) - { - if (comparer.Equals(entry.Key, key)) - { - return true; - } - } - - return false; - } - - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - foreach (var entry in entries) - { - if (comparer.Equals(entry.Key, key)) - { - value = entry.Value; - return true; - } - } - - value = default; - return false; - } - - public bool Remove(TKey key) - { - for (var i = 0; i < entries.Count; i++) - { - var entry = entries[i]; - - if (comparer.Equals(entry.Key, key)) - { - entries.RemoveAt(i); - return true; - } - } - - return false; - } - - public bool Remove(KeyValuePair item) - { - for (var i = 0; i < entries.Count; i++) - { - var entry = entries[i]; - - if (comparer.Equals(entry.Key, item.Key) && Equals(entry.Value, item.Value)) - { - entries.RemoveAt(i); - return true; - } - } - - return false; - } - - public void TrimExcess() - { - entries.TrimExcess(); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - entries.CopyTo(array, arrayIndex); - } - - public IEnumerator> GetEnumerator() - { - return new Enumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(this); - } -} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs index f304015bd..c555f0409 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs @@ -240,7 +240,7 @@ public static class ValueConverter if (value.Value is string s) { - if (Guid.TryParse(s, out result)) + if (Guid.TryParseExact(s, "D", out result)) { return true; } @@ -301,7 +301,7 @@ public static class ValueConverter return true; case string s: { - if (Guid.TryParse(s, out var guid)) + if (Guid.TryParseExact(s, "D", out var guid)) { result = guid; diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index d5d44494d..22df15365 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable en enable diff --git a/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs b/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs index d9ded0b5f..af854c78a 100644 --- a/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/SchemaResolver.cs @@ -68,7 +68,7 @@ public sealed class SchemaResolver : IAsyncActionFilter { var canCache = !user.IsInClient(DefaultClients.Frontend); - if (Guid.TryParse(schemaIdOrName, out var guid)) + if (Guid.TryParseExact(schemaIdOrName, "D", out var guid)) { var schemaId = DomainId.Create(guid); diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index b2e344aa3..8ae350cb9 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -1,7 +1,7 @@  net7.0 - 10.0 + 11.0 enable enable diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs index 0781b7192..82f4b7ab9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs @@ -24,17 +24,17 @@ public sealed class AppImageController : ApiController { private readonly IAppImageStore appImageStore; private readonly IAssetStore assetStore; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IAssetThumbnailGenerator assetGenerator; public AppImageController(ICommandBus commandBus, IAppImageStore appImageStore, IAssetStore assetStore, - IAssetThumbnailGenerator assetThumbnailGenerator) + IAssetThumbnailGenerator assetGenerator) : base(commandBus) { this.appImageStore = appImageStore; this.assetStore = assetStore; - this.assetThumbnailGenerator = assetThumbnailGenerator; + this.assetGenerator = assetGenerator; } /// @@ -112,7 +112,7 @@ public sealed class AppImageController : ApiController { await using (var resizeStream = assetResized.OpenWrite()) { - await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, mimeType, resizeStream, resizeOptions, ct); + await assetGenerator.CreateThumbnailAsync(originalStream, mimeType, resizeStream, resizeOptions, ct); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 3f8eb004e..9ea321287 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -29,20 +29,20 @@ public sealed class AssetContentController : ApiController private readonly IAssetFileStore assetFileStore; private readonly IAssetQueryService assetQuery; private readonly IAssetLoader assetLoader; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IAssetThumbnailGenerator assetGenerator; public AssetContentController( ICommandBus commandBus, IAssetFileStore assetFileStore, IAssetQueryService assetQuery, IAssetLoader assetLoader, - IAssetThumbnailGenerator assetThumbnailGenerator) + IAssetThumbnailGenerator assetGenerator) : base(commandBus) { this.assetFileStore = assetFileStore; this.assetQuery = assetQuery; this.assetLoader = assetLoader; - this.assetThumbnailGenerator = assetThumbnailGenerator; + this.assetGenerator = assetGenerator; } /// @@ -138,13 +138,13 @@ public sealed class AssetContentController : ApiController Response.Headers[HeaderNames.CacheControl] = $"public,max-age={request.CacheDuration}"; } - var resizeOptions = request.ToResizeOptions(asset, assetThumbnailGenerator, HttpContext.Request); + var resizeOptions = request.ToResizeOptions(asset, assetGenerator, HttpContext.Request); var contentLength = (long?)null; var contentCallback = (FileCallback?)null; var contentType = asset.MimeType; - if (asset.Type == AssetType.Image && assetThumbnailGenerator.IsResizable(asset.MimeType, resizeOptions, out var destinationMimeType)) + if (asset.Type == AssetType.Image && assetGenerator.IsResizable(asset.MimeType, resizeOptions, out var destinationMimeType)) { contentType = destinationMimeType!; @@ -224,7 +224,7 @@ public sealed class AssetContentController : ApiController { await using (var resizeStream = assetResized.OpenWrite()) { - await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, asset.MimeType, resizeStream, resizeOptions); + await assetGenerator.CreateThumbnailAsync(originalStream, asset.MimeType, resizeStream, resizeOptions); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs index c2daa2e74..a3c320469 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs @@ -99,7 +99,7 @@ public sealed class AssetContentQueryDto [FromQuery(Name = "format")] public ImageFormat? Format { get; set; } - public ResizeOptions ToResizeOptions(IAssetEntity asset, IAssetThumbnailGenerator assetThumbnailGenerator, HttpRequest request) + public ResizeOptions ToResizeOptions(IAssetEntity asset, IAssetThumbnailGenerator assetGenerator, HttpRequest request) { Guard.NotNull(asset); @@ -111,12 +111,12 @@ public sealed class AssetContentQueryDto result.FocusY = y; result.TargetWidth = Width; result.TargetHeight = Height; - result.Format = GetFormat(asset, assetThumbnailGenerator, request); + result.Format = GetFormat(asset, assetGenerator, request); return result; } - private ImageFormat? GetFormat(IAssetEntity asset, IAssetThumbnailGenerator assetThumbnailGenerator, HttpRequest request) + private ImageFormat? GetFormat(IAssetEntity asset, IAssetThumbnailGenerator assetGenerator, HttpRequest request) { if (Format.HasValue || !Auto) { @@ -132,7 +132,7 @@ public sealed class AssetContentQueryDto request.Headers.TryGetValue("Accept", out var accept); - return accept.Any(x => x?.Contains(mimeType, StringComparison.OrdinalIgnoreCase) == true) && assetThumbnailGenerator.CanReadAndWrite(mimeType); + return accept.Any(x => x?.Contains(mimeType, StringComparison.OrdinalIgnoreCase) == true) && assetGenerator.CanReadAndWrite(mimeType); } #if ENABLE_AVIF if (Accepts("image/avif")) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 7396df0bf..e6a3f8a81 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -383,7 +383,7 @@ public sealed class SchemasController : ApiController private Task GetSchemaAsync(string schema) { - if (Guid.TryParse(schema, out var guid)) + if (Guid.TryParseExact(schema, "D", out var guid)) { var schemaId = DomainId.Create(guid); diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index b71f50b30..d773eab44 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -30,19 +30,19 @@ public sealed class ProfileController : IdentityServerController { private readonly IUserPictureStore userPictureStore; private readonly IUserService userService; - private readonly IAssetThumbnailGenerator assetThumbnailGenerator; + private readonly IAssetThumbnailGenerator assetGenerator; private readonly MyIdentityOptions identityOptions; public ProfileController( IOptions identityOptions, IUserPictureStore userPictureStore, IUserService userService, - IAssetThumbnailGenerator assetThumbnailGenerator) + IAssetThumbnailGenerator assetGenerator) { this.identityOptions = identityOptions.Value; this.userPictureStore = userPictureStore; this.userService = userService; - this.assetThumbnailGenerator = assetThumbnailGenerator; + this.assetGenerator = assetGenerator; } [HttpGet] @@ -183,7 +183,7 @@ public sealed class ProfileController : IdentityServerController { await using (var resizeStream = assetResized.OpenWrite()) { - await assetThumbnailGenerator.CreateThumbnailAsync(originalStream, file.ContentType, resizeStream, resizeOptions, ct); + await assetGenerator.CreateThumbnailAsync(originalStream, file.ContentType, resizeStream, resizeOptions, ct); } } } diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index aa831a4fb..8e4b464da 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -46,7 +46,7 @@ public static class AssetServices services.AddTransientAs() .As(); - services.AddTransientAs() + services.AddSingletonAs() .As(); services.AddSingletonAs() diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 93aa9d271..dbf6c5c8c 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -41,7 +41,7 @@ public static class ContentsServices services.AddTransientAs() .As().As(); - services.AddTransientAs() + services.AddSingletonAs() .As(); services.AddSingletonAs() diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 1ae4eec5b..00456b0de 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -107,6 +107,17 @@ public static class WebServices builder.AddSchema(); builder.AddSystemTextJson(); builder.AddDataLoader(); + builder.ConfigureExecutionOptions(options => + { + var logger = options.RequestServices!.GetRequiredService>(); + + options.UnhandledExceptionDelegate = ctx => + { + logger.LogError(ctx.Exception, "GraphQL error in field {field}", ctx.FieldContext?.FieldAst?.Name); + + return Task.CompletedTask; + }; + }); }); services.AddSingletonAs() diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 7f8d676f7..bb9a3ce16 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -3,7 +3,7 @@ net7.0 Latest true - 10.0 + 11.0 enable en NU1608 diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index fb7164098..aa13dda17 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -338,6 +338,9 @@ // The number of days request log items will be stored. "storeRetentionInDays": 90, + // The name that is used for monitoring. + "name": "Squidex", + "stackdriver": { // True, to enable stackdriver integration. "enabled": false, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/AssetMetadataWrapperTests.cs similarity index 89% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/AssetMetadataWrapperTests.cs index cdb7d70e7..5e53c1f69 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/AssetMetadataWrapperTests.cs @@ -6,18 +6,19 @@ // ========================================================================== using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Core.Scripting.Internal; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; +namespace Squidex.Domain.Apps.Core.Operations.Scripting; -public class ScriptMetadataWrapperTests +public class AssetMetadataWrapperTests { private readonly AssetMetadata metadata = new AssetMetadata(); - private readonly ScriptMetadataWrapper sut; + private readonly AssetMetadataWrapper sut; - public ScriptMetadataWrapperTests() + public AssetMetadataWrapperTests() { - sut = new ScriptMetadataWrapper(metadata); + sut = new AssetMetadataWrapper(metadata); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index d4751117b..faac8e0ed 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -537,10 +537,7 @@ public class JintScriptEngineTests : IClassFixture [Fact] public void Should_not_allow_to_overwrite_initial_var() { - var vars = new ScriptVars - { - ["value"] = 13 - }; + var vars = new ScriptVars().SetInitial(13, "value"); const string script = @" ctx.value = ctx.value * 2; diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs index 2e39ec23d..38ac4870c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs @@ -101,6 +101,7 @@ public class ScriptingCompleterTests "ctx.asset.parentId", "ctx.asset.parentPath", "ctx.asset.tags", + "ctx.asset.type", "ctx.assetId", "ctx.command", "ctx.command.fileHash", diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs index a9e6ad051..fb274f3a6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs @@ -127,6 +127,19 @@ public class ComponentFieldTests : IClassFixture Assert.Equal(value.AsObject[Component.Discriminator].AsString, schemaId1.ToString()); } + [Fact] + public async Task Should_resolve_schema_id_from_name_id_id() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + + var value = CreateValue("my-component", "componentField", 1); + + await sut.ValidateAsync(value, errors, components: components); + + Assert.Empty(errors); + Assert.Equal(value.AsObject[Component.Discriminator].AsString, schemaId1.ToString()); + } + [Fact] public async Task Should_resolve_schema_from_single_component() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index db08fd3fc..fad96e852 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -3,7 +3,7 @@ Exe net7.0 Squidex.Domain.Apps.Core - 10.0 + 11.0 enable enable diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index 66c736381..e7f7715cb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -18,7 +18,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase { private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly IAppImageStore appImageStore = A.Fake(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetThumbnailGenerator assetGenerator = A.Fake(); private readonly AppCommandMiddleware sut; public sealed class MyCommand : SquidexCommand @@ -32,7 +32,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase public AppCommandMiddlewareTests() { - sut = new AppCommandMiddleware(domainObjectFactory, appImageStore, assetThumbnailGenerator, ApiContextProvider); + sut = new AppCommandMiddleware(domainObjectFactory, appImageStore, assetGenerator, ApiContextProvider); } [Fact] @@ -50,7 +50,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase { var file = new NoopAssetFile(); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) .Returns(new ImageInfo(ImageFormat.PNG, 100, 100, ImageOrientation.None, false)); await HandleAsync(new UploadAppImage { File = file }, None.Value); @@ -66,7 +66,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase var command = new UploadAppImage { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) .Returns(Task.FromResult(null)); await Assert.ThrowsAsync(() => HandleAsync(sut, command, CancellationToken)); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index e065c1a91..2ca5037f1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -23,7 +23,7 @@ public class AssetsFluidExtensionTests : GivenContext { private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetThumbnailGenerator assetGenerator = A.Fake(); private readonly FluidTemplateEngine sut; public AssetsFluidExtensionTests() @@ -33,7 +33,7 @@ public class AssetsFluidExtensionTests : GivenContext .AddSingleton(AppProvider) .AddSingleton(assetFileStore) .AddSingleton(assetQuery) - .AddSingleton(assetThumbnailGenerator) + .AddSingleton(assetGenerator) .BuildServiceProvider(); var extensions = new IFluidExtension[] @@ -146,7 +146,7 @@ public class AssetsFluidExtensionTests : GivenContext [Fact] public async Task Should_not_resolve_text_if_too_big() { - var (vars, _) = SetupAssetVars(1_000_000); + var (vars, _) = SetupAssetVars(10_000_000); var template = @" {% assign ref = event.data.assets.iv[0] | asset %} @@ -221,7 +221,7 @@ public class AssetsFluidExtensionTests : GivenContext [Fact] public async Task Should_not_resolve_blur_hash_if_too_big() { - var (vars, _) = SetupAssetVars(1_000_000); + var (vars, _) = SetupAssetVars(10_000_000); var template = @" {% assign ref = event.data.assets.iv[0] | asset %} @@ -229,7 +229,7 @@ public class AssetsFluidExtensionTests : GivenContext "; var expected = $@" - Text: ErrorTooBig + Text: "; var actual = await sut.RenderAsync(template, vars); @@ -251,7 +251,7 @@ public class AssetsFluidExtensionTests : GivenContext "; var expected = $@" - Text: NoImage + Text: "; var actual = await sut.RenderAsync(template, vars); @@ -296,7 +296,7 @@ public class AssetsFluidExtensionTests : GivenContext private void SetupBlurHash(AssetRef asset, string hash) { - A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A._, asset.MimeType, A._, A._)) + A.CallTo(() => assetGenerator.ComputeBlurHashAsync(A._, asset.MimeType, A._, A._)) .Returns(hash); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index c9fb19b28..19713054b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -16,17 +16,20 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Assets; public class AssetsJintExtensionTests : GivenContext, IClassFixture { + private readonly ICommandBus commandBus = A.Fake(); private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetThumbnailGenerator assetGenerator = A.Fake(); private readonly JintScriptEngine sut; public AssetsJintExtensionTests() @@ -34,9 +37,10 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture commandBus.PublishAsync( + A.That.Matches(x => x.AssetId == @event.Id && x.Metadata!.Count == 3), default)) + .MustHaveHappened(); + } + private void SetupBlurHash(AssetRef asset, string hash) { - A.CallTo(() => assetThumbnailGenerator.ComputeBlurHashAsync(A._, asset.MimeType, A._, A._)) + A.CallTo(() => assetGenerator.ComputeBlurHashAsync(A._, asset.MimeType, A._, A._)) .Returns(hash); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs index 46528796f..78802704b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs @@ -98,7 +98,7 @@ public class DefaultAssetFileStoreTests : GivenContext } [Fact] - public async Task Should_upload_temporary_filet_to_store() + public async Task Should_upload_temporary_file_to_store() { var stream = new MemoryStream(); @@ -124,6 +124,17 @@ public class DefaultAssetFileStoreTests : GivenContext .MustHaveHappened(); } + [Fact] + public async Task Should_download_temporary_file_to_store() + { + var stream = new MemoryStream(); + + await sut.DownloadAsync("Temp", stream, CancellationToken); + + A.CallTo(() => assetStore.DownloadAsync("Temp", stream, default, CancellationToken)) + .MustHaveHappened(); + } + [Theory] [MemberData(nameof(PathCases))] public async Task Should_download_file_from_store(bool folderPerApp, string? suffix, string fileName) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index c85cc15b7..3a9b46fb2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets; public class ImageAssetMetadataSourceTests : GivenContext { - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetThumbnailGenerator assetGenerator = A.Fake(); private readonly MemoryStream stream = new MemoryStream(); private readonly AssetFile file; private readonly ImageAssetMetadataSource sut; @@ -24,7 +24,7 @@ public class ImageAssetMetadataSourceTests : GivenContext { file = new DelegateAssetFile("MyImage.png", "image/png", 1024, () => stream); - sut = new ImageAssetMetadataSource(assetThumbnailGenerator); + sut = new ImageAssetMetadataSource(assetGenerator); } [Fact] @@ -34,7 +34,7 @@ public class ImageAssetMetadataSourceTests : GivenContext await sut.EnhanceAsync(command, CancellationToken); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) .MustHaveHappened(); } @@ -43,7 +43,7 @@ public class ImageAssetMetadataSourceTests : GivenContext { var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) .Returns(Task.FromResult(null)); await sut.EnhanceAsync(command, CancellationToken); @@ -56,7 +56,7 @@ public class ImageAssetMetadataSourceTests : GivenContext { var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false)); await sut.EnhanceAsync(command, CancellationToken); @@ -65,7 +65,7 @@ public class ImageAssetMetadataSourceTests : GivenContext Assert.Equal(600, command.Metadata.GetPixelHeight()); Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A._, A._)) + A.CallTo(() => assetGenerator.FixAsync(stream, file.MimeType, A._, A._)) .MustNotHaveHappened(); } @@ -74,10 +74,10 @@ public class ImageAssetMetadataSourceTests : GivenContext { var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false)); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.BottomRight, false)).Once(); await sut.EnhanceAsync(command, CancellationToken); @@ -86,7 +86,7 @@ public class ImageAssetMetadataSourceTests : GivenContext Assert.Equal(600, command.Metadata.GetPixelHeight()); Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A._, CancellationToken)) + A.CallTo(() => assetGenerator.FixAsync(stream, file.MimeType, A._, CancellationToken)) .MustHaveHappened(); } @@ -95,10 +95,10 @@ public class ImageAssetMetadataSourceTests : GivenContext { var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(A._, file.MimeType, CancellationToken)) .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false)); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) + A.CallTo(() => assetGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken)) .Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, true)).Once(); await sut.EnhanceAsync(command, CancellationToken); @@ -107,7 +107,7 @@ public class ImageAssetMetadataSourceTests : GivenContext Assert.Equal(600, command.Metadata.GetPixelHeight()); Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A._, CancellationToken)) + A.CallTo(() => assetGenerator.FixAsync(stream, file.MimeType, A._, CancellationToken)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index dc496705a..87313fab7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -3,7 +3,7 @@ Exe net7.0 Squidex.Domain.Apps.Entities - 10.0 + 11.0 enable enable en diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index 0242c0923..4e0261ee2 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -3,7 +3,7 @@ Exe net7.0 Squidex.Domain.Users - 10.0 + 11.0 enable enable diff --git a/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs index 54cc3cfe3..136f9c376 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs @@ -8,171 +8,43 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -#pragma warning disable SA1313 // Parameter names should begin with lower-case letter - namespace Squidex.Infrastructure.Caching; public class QueryCacheTests { private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private record CachedEntry(int Value) : IWithId - { - public int Id => Value; - } - - [Fact] - public async Task Should_query_from_cache() - { - var sut = new QueryCache(); - - var (queried, actual) = await ConfigureAsync(sut, 1, 2); - - Assert.Equal(new[] { 1, 2 }, queried); - Assert.Equal(new[] { 1, 2 }, actual); - } - - [Fact] - public async Task Should_query_pending_from_cache() - { - var sut = new QueryCache(); - - var (queried1, actual1) = await ConfigureAsync(sut, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); - - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - - Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); - } - - [Fact] - public async Task Should_query_pending_from_cache_if_manually_added() - { - var sut = new QueryCache(); - - sut.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); - - var (queried, actual) = await ConfigureAsync(sut, 1, 2, 3, 4); - - Assert.Equal(new[] { 3, 4 }, queried); - Assert.Equal(new[] { 2, 3, 4 }, actual); - } - [Fact] - public async Task Should_query_pending_from_memory_cache_if_manually_added() + public void Should_query_from_cache() { - var sut1 = new QueryCache(memoryCache); - var sut2 = new QueryCache(memoryCache); + var sut = new QueryCache(memoryCache); - var cacheDuration = TimeSpan.FromSeconds(10); + sut.Set(1, 1, TimeSpan.FromHours(1)); - sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); + var result1 = sut.TryGet(1, out var found1); + var result2 = sut.TryGet(2, out var found2); - var (queried, actual) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); + Assert.True(result1); + Assert.Equal(1, found1); - Assert.Equal(new[] { 3, 4 }, queried); - Assert.Equal(new[] { 2, 3, 4 }, actual); + Assert.False(result2); + Assert.Equal(0, found2); } [Fact] - public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_added_permanently() - { - var sut1 = new QueryCache(memoryCache); - var sut2 = new QueryCache(memoryCache); - - sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); - - var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); - - Assert.Equal(new[] { 1, 2, 3, 4 }, queried); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual); - } - - [Fact] - public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_queried_permanently() - { - var sut1 = new QueryCache(memoryCache); - var sut2 = new QueryCache(memoryCache); - - var cacheDuration = TimeSpan.FromSeconds(10); - - sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); - - var (queried, actual) = await ConfigureAsync(sut2, 1, 2, 3, 4); - - Assert.Equal(new[] { 1, 2, 3, 4 }, queried); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual); - } - - [Fact] - public async Task Should_not_query_again_if_failed_before() - { - var sut = new QueryCache(); - - var (queried1, actual1) = await ConfigureAsync(sut, x => x > 1, default, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut, 1, 2, 3, 4); - - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - - Assert.Equal(new[] { 2 }, actual1.ToArray()); - Assert.Equal(new[] { 2, 3, 4 }, actual2.ToArray()); - } - - [Fact] - public async Task Should_query_from_memory_cache() - { - var sut1 = new QueryCache(memoryCache); - var sut2 = new QueryCache(memoryCache); - - var cacheDuration = TimeSpan.FromSeconds(10); - - var (queried1, actual1) = await ConfigureAsync(sut1, x => true, cacheDuration, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); - - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); - - Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); - } - - [Fact] - public async Task Should_not_query_from_memory_cache_if_not_queried_permanently() - { - var sut1 = new QueryCache(memoryCache); - var sut2 = new QueryCache(memoryCache); - - var (queried1, actual1) = await ConfigureAsync(sut1, x => true, null, 1, 2); - var (queried2, actual2) = await ConfigureAsync(sut2, x => true, null, 1, 2, 3, 4); - - Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, queried2.ToArray()); - - Assert.Equal(new[] { 1, 2 }, actual1.ToArray()); - Assert.Equal(new[] { 1, 2, 3, 4 }, actual2.ToArray()); - } - - private static Task<(int[], int[])> ConfigureAsync(IQueryCache sut, params int[] ids) - { - return ConfigureAsync(sut, x => true, null, ids); - } - - private static async Task<(int[], int[])> ConfigureAsync(IQueryCache sut, Func predicate, TimeSpan? cacheDuration, params int[] ids) + public void Should_not_query_from_cache_if_not_configured() { - var queried = new HashSet(); + var sut = new QueryCache(); - var actual = await sut.CacheOrQueryAsync(ids, async pending => - { - queried.AddRange(pending); + sut.Set(1, 1, TimeSpan.FromHours(1)); - await Task.Yield(); + var result1 = sut.TryGet(1, out var found1); + var result2 = sut.TryGet(2, out var found2); - return pending.Where(predicate).Select(x => new CachedEntry(x)); - }, cacheDuration); + Assert.False(result1); + Assert.Equal(0, found1); - return (queried.ToArray(), actual.Select(x => x.Value).ToArray()); + Assert.False(result2); + Assert.Equal(0, found2); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs deleted file mode 100644 index 3e3828fc1..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs +++ /dev/null @@ -1,478 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections; -using Squidex.Infrastructure.TestHelpers; - -#pragma warning disable xUnit2017 // Do not use Contains() to check if a value exists in a collection -#pragma warning disable IDE0028 // Simplify collection initialization -#pragma warning disable CA1841 // Prefer Dictionary.Contains methods - -namespace Squidex.Infrastructure.Collections; - -public class ListDictionaryTests -{ - [Fact] - public void Should_create_empty() - { - var sut = new ListDictionary(); - - Assert.Empty(sut); - Assert.Equal(1, sut.Capacity); - } - - [Fact] - public void Should_create_with_capacity() - { - var sut = new ListDictionary(20); - - Assert.Empty(sut); - Assert.Equal(20, sut.Capacity); - } - - [Fact] - public void Should_create_as_copy() - { - var source = new ListDictionary(); - - source.Add(1, 10); - source.Add(2, 20); - - var sut = new ListDictionary(source); - - Assert.Equal(2, sut.Count); - } - - [Fact] - public void Should_not_be_readonly() - { - var sut = new ListDictionary(); - - Assert.False(sut.IsReadOnly); - Assert.False(sut.Keys.IsReadOnly); - Assert.False(sut.Values.IsReadOnly); - } - - [Fact] - public void Should_add_item() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } - - [Fact] - public void Should_add_item_unsafe() - { - var sut = new ListDictionary(); - - sut.AddUnsafe(1, 10); - - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } - - [Fact] - public void Should_add_item_as_pair() - { - var sut = new ListDictionary(); - - sut.Add(new KeyValuePair(1, 10)); - - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } - - [Fact] - public void Should_throw_exception_if_adding_existing_key() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - - Assert.Throws(() => sut.Add(1, 20)); - } - - [Fact] - public void Should_throw_exception_if_adding_pair_with_existing_key() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - - Assert.Throws(() => sut.Add(new KeyValuePair(1, 20))); - } - - [Fact] - public void Should_set_item() - { - var sut = new ListDictionary(); - - sut[1] = 10; - - Assert.Single(sut); - Assert.Equal(10, sut[1]); - } - - [Fact] - public void Should_override_item() - { - var sut = new ListDictionary(); - - sut[1] = 20; - - Assert.Single(sut); - Assert.Equal(20, sut[1]); - } - - [Fact] - public void Should_return_true_when_dictionary_contains_value() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - - Assert.True(sut.Contains(new KeyValuePair(1, 10))); - Assert.True(sut.ContainsKey(1)); - Assert.True(sut.Keys.Contains(1)); - Assert.True(sut.Values.Contains(10)); - } - - [Fact] - public void Should_return_false_when_dictionary_does_not_contains_value() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - - Assert.False(sut.Contains(new KeyValuePair(1, 20))); - Assert.False(sut.ContainsKey(2)); - Assert.False(sut.Keys.Contains(2)); - Assert.False(sut.Values.Contains(20)); - } - - [Fact] - public void Should_get_count() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - - Assert.Equal(3, sut.Count); - Assert.Equal(3, sut.Keys.Count); - Assert.Equal(3, sut.Values.Count); - } - - [Fact] - public void Should_clear() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - sut.Clear(); - - Assert.Empty(sut); - } - - [Fact] - public void Should_remove_key() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - - Assert.True(sut.Remove(2)); - Assert.False(sut.ContainsKey(2)); - } - - [Fact] - public void Should_not_remove_key_if_not_found() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - - Assert.False(sut.Remove(4)); - } - - [Fact] - public void Should_remove_item() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - - Assert.True(sut.Remove(new KeyValuePair(2, 20))); - Assert.False(sut.ContainsKey(2)); - } - - [Fact] - public void Should_not_remove_item_if_key_not_found() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - - Assert.False(sut.Remove(new KeyValuePair(4, 40))); - } - - [Fact] - public void Should_not_remove_item_if_value_not_equal() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - sut.Add(3, 30); - - Assert.False(sut.Remove(new KeyValuePair(2, 40))); - } - - [Fact] - public void Should_get_value_by_method_if_found() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - Assert.True(sut.TryGetValue(2, out var found)); - Assert.Equal(20, found); - } - - [Fact] - public void Should_not_get_value_by_method_if_not_found() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(3, 30); - - Assert.False(sut.TryGetValue(4, out var found)); - Assert.Equal(0, found); - } - - [Fact] - public void Should_get_value_by_indexer_if_found() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - Assert.Equal(20, sut[2]); - } - - [Fact] - public void Should_not_get_value_by_indexer_if_not_found() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - Assert.Throws(() => sut[4]); - } - - [Fact] - public void Should_loop_over_entries() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - var actual = new List>(); - - foreach (var entry in sut) - { - actual.Add(entry); - } - - Assert.Equal(new[] - { - new KeyValuePair(1, 10), - new KeyValuePair(2, 20) - }, actual.ToArray()); - } - - [Fact] - public void Should_loop_over_entries_with_old_enumerator() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - var actual = new List>(); - - foreach (KeyValuePair entry in (IEnumerable)sut) - { - actual.Add(entry); - } - - Assert.Equal(new[] - { - new KeyValuePair(1, 10), - new KeyValuePair(2, 20) - }, actual.ToArray()); - } - - [Fact] - public void Should_copy_entries_to_array() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - Assert.Equal(new[] - { - new KeyValuePair(1, 10), - new KeyValuePair(2, 20) - }, sut.ToArray()); - } - - [Fact] - public void Should_loop_over_keys() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - var actual = new List(); - - foreach (var entry in sut.Keys) - { - actual.Add(entry); - } - - Assert.Equal(new[] { 1, 2 }, actual.ToArray()); - } - - [Fact] - public void Should_loop_over_keys_with_old_enumerator() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - var actual = new List(); - - foreach (int entry in (IEnumerable)sut.Keys) - { - actual.Add(entry); - } - - Assert.Equal(new[] { 1, 2 }, actual.ToArray()); - } - - [Fact] - public void Should_copy_keys_to_array() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - Assert.Equal(new[] { 1, 2 }, sut.Keys.ToArray()); - } - - [Fact] - public void Should_loop_over_values() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - var actual = new List(); - - foreach (var entry in sut.Values) - { - actual.Add(entry); - } - - Assert.Equal(new[] { 10, 20 }, actual.ToArray()); - } - - [Fact] - public void Should_loop_over_values_with_old_enumerator() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - var actual = new List(); - - foreach (int entry in (IEnumerable)sut.Values) - { - actual.Add(entry); - } - - Assert.Equal(new[] { 10, 20 }, actual.ToArray()); - } - - [Fact] - public void Should_copy_values_to_array() - { - var sut = new ListDictionary(); - - sut.Add(1, 10); - sut.Add(2, 20); - - Assert.Equal(new[] { 10, 20 }, sut.Values.ToArray()); - } - - [Fact] - public void Should_trim() - { - var sut = new ListDictionary(20); - - sut.Add(1, 10); - sut.Add(2, 20); - - Assert.Equal(20, sut.Capacity); - - sut.TrimExcess(); - - Assert.Equal(2, sut.Capacity); - } - - [Fact] - public void Should_serialize_and_deserialize() - { - var sut = new Dictionary - { - [11] = 1, - [12] = 2, - [13] = 3 - }.ToReadonlyDictionary(); - - var serialized = sut.SerializeAndDeserialize(); - - Assert.Equal(sut, serialized); - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index bf287b5f1..6a81fbfc9 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -3,7 +3,7 @@ Exe net7.0 Squidex.Infrastructure - 10.0 + 11.0 enable enable en diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj index 7f6eb70b8..2c8d82ea6 100644 --- a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -3,7 +3,7 @@ Exe net7.0 Squidex.Web - 10.0 + 11.0 enable enable diff --git a/backend/tools/GenerateLanguages/GenerateLanguages.csproj b/backend/tools/GenerateLanguages/GenerateLanguages.csproj index b3f33210f..46f5dbfe3 100644 --- a/backend/tools/GenerateLanguages/GenerateLanguages.csproj +++ b/backend/tools/GenerateLanguages/GenerateLanguages.csproj @@ -2,7 +2,7 @@ Exe net7.0 - 10.0 + 11.0 enable diff --git a/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index efba03c85..08c140075 100644 --- a/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -378,10 +378,11 @@ public class AssetTests : IClassFixture // STEP 4: Check tags. - var tags = await _.Client.Assets.WaitForTagsAsync(tag1, TimeSpan.FromMinutes(2)); + var tags = await _.Client.Assets.PollTagAsync(tag1); Assert.Contains(tag1, tags); Assert.Contains(tag2, tags); + Assert.Equal(1, tags[tag1]); Assert.Equal(1, tags[tag2]); @@ -479,7 +480,7 @@ public class AssetTests : IClassFixture var asset_1 = await app.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png", parentId: folder.Id); - // STEP 3: Download asset. + // STEP 3: Download asset before script. await using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) { var downloaded = await _.DownloadAsync(asset_1); @@ -516,6 +517,56 @@ public class AssetTests : IClassFixture await Verify(asset_1); } + [Fact] + public async Task Should_compute_blur_hash_script() + { + // STEP 0: Create app. + var (app, _) = await _.PostAppAsync(); + + + // STEP 1: Create folder. + var folderRequest = new CreateAssetFolderDto + { + FolderName = "folder" + }; + + var folder = await app.Assets.PostAssetFolderAsync(folderRequest); + + + // STEP 2: Set script to calculate blur hash + var scriptsRequest = new UpdateAssetScriptsDto + { + Create = @" + if (ctx.asset.type === 'Image') { + getAssetBlurHash(ctx.asset, function (hash) { + ctx.command.metadata['blurHash'] = hash; + }); + }", + Update = @" + if (ctx.asset.type === 'Image') { + getAssetBlurHash(ctx.asset, function (hash) { + ctx.command.metadata['blurHash'] = hash; + }); + }" + }; + + await app.Apps.PutAssetScriptsAsync(scriptsRequest); + + + // STEP 3: Create asset. + var asset_1 = await app.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png", parentId: folder.Id); + + + // STEP 4: Create asset. + var asset_2 = await app.Assets.UpdateFileAsync(asset_1.Id, "Assets/logo-wide.png", "image/png"); + + Assert.NotNull(asset_1.Metadata["blurHash"]); + Assert.NotNull(asset_2.Metadata["blurHash"]); + Assert.NotEqual(asset_1.Metadata["blurHash"], asset_2.Metadata["blurHash"]); + + await Verify(asset_2); + } + [Fact] public async Task Should_query_asset_by_metadata() { @@ -626,7 +677,7 @@ public class AssetTests : IClassFixture await _.Client.Assets.DeleteAssetFolderAsync(folder_1.Id); // Ensure that asset in folder is deleted. - Assert.True(await _.Client.Assets.WaitForDeletionAsync(asset_1.Id, TimeSpan.FromSeconds(30))); + Assert.True(await _.Client.Assets.PollForDeletionAsync(asset_1.Id)); // Ensure that other asset is not deleted. Assert.NotNull(await _.Client.Assets.GetAssetAsync(asset_2.Id)); diff --git a/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs b/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs index d24aafcf3..05d36ea02 100644 --- a/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs @@ -45,8 +45,7 @@ public class BackupTests : IClassFixture // STEP 2: Create backup. await app.Backups.PostBackupAsync(); - var backups = await app.Backups.WaitForBackupsAsync(x => x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); - var backup = backups.FirstOrDefault(x => x.Status is JobStatus.Completed or JobStatus.Failed); + var backup = await app.Backups.PollAsync(x => x.Status is JobStatus.Completed or JobStatus.Failed); Assert.Equal(JobStatus.Completed, backup?.Status); @@ -65,7 +64,7 @@ public class BackupTests : IClassFixture // STEP 4: Wait for the backup. - var restore = await app.Backups.WaitForRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); + var restore = await app.Backups.PollRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed); Assert.Equal(JobStatus.Completed, restore?.Status); } @@ -87,8 +86,7 @@ public class BackupTests : IClassFixture // STEP 2: Create backup. await app.Backups.PostBackupAsync(); - var backups = await app.Backups.WaitForBackupsAsync(x => x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); - var backup = backups.FirstOrDefault(x => x.Status is JobStatus.Completed or JobStatus.Failed); + var backup = await app.Backups.PollAsync(x => x.Status is JobStatus.Completed or JobStatus.Failed); Assert.Equal(JobStatus.Completed, backup?.Status); @@ -111,7 +109,7 @@ public class BackupTests : IClassFixture // STEP 5: Wait for the backup. - var restore = await app.Backups.WaitForRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed, TimeSpan.FromMinutes(2)); + var restore = await app.Backups.PollRestoreAsync(x => x.Url == uri && x.Status is JobStatus.Completed or JobStatus.Failed); Assert.Equal(JobStatus.Completed, restore?.Status); } diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 45ab1d5b7..abcc109c5 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -362,7 +362,7 @@ public class ContentQueryTests : IClassFixture { var q = new ContentQuery { Search = "2" }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.PollAsync(q, x => true); AssertItems(items, 1, new[] { 2 }); } @@ -378,7 +378,7 @@ public class ContentQueryTests : IClassFixture } }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.PollAsync(q, x => true); AssertItems(items, 1, new[] { 2 }); } @@ -388,7 +388,7 @@ public class ContentQueryTests : IClassFixture { var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(103 3)') lt 1000" }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.PollAsync(q, x => true); AssertItems(items, 1, new[] { 3 }); } @@ -414,7 +414,7 @@ public class ContentQueryTests : IClassFixture } }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.PollAsync(q, x => true); AssertItems(items, 1, new[] { 3 }); } @@ -424,7 +424,7 @@ public class ContentQueryTests : IClassFixture { var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(104 4)') lt 1000" }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.PollAsync(q, x => true); AssertItems(items, 1, new[] { 4 }); } @@ -499,7 +499,7 @@ public class ContentQueryTests : IClassFixture } }; - var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); + var items = await _.Contents.PollAsync(q, x => true); AssertItems(items, 1, new[] { 4 }); } diff --git a/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs b/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs index cc435fbcd..e4ea44fda 100644 --- a/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/HistoryTests.cs @@ -23,8 +23,8 @@ public class HistoryTests : IClassFixture [Fact] public async Task Should_get_history() { - var history = await _.Client.History.WaitForHistoryAsync(null, x => true, TimeSpan.FromSeconds(30)); + var history = await _.Client.History.PollAsync(null, x => true); - Assert.NotEmpty(history); + Assert.NotNull(history); } } diff --git a/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs b/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs index 272f2dc8e..dc0cd4b28 100644 --- a/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs @@ -47,11 +47,9 @@ public class RuleRunnerTests : IClassFixture, IClassFixture, IClassFixture x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + var request = await webhookCatcher.PollAsync(sessionId, x => x.IsPost() && x.HasContent(schemaName)); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + AssertRequest(request); // STEP 4: Get events. @@ -123,9 +118,6 @@ public class RuleRunnerTests : IClassFixture, IClassFixture, IClassFixture, IClassFixture x.Method == "POST" && x.Content.Contains(updatedString, StringComparison.OrdinalIgnoreCase) && x.Content.Contains(updateEvent, StringComparison.OrdinalIgnoreCase)); + var request = await webhookCatcher.PollAsync(sessionId, x => x.IsPost() && x.HasContent(updatedString) && x.HasContent(updateEvent)); - Assert.NotNull(request); + AssertRequest(request); // STEP 4: Get events. @@ -216,8 +210,7 @@ public class RuleRunnerTests : IClassFixture, IClassFixture x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + var request = await webhookCatcher.PollAsync(sessionId, x => x.IsPost() && x.HasContent(schemaName)); Assert.NotNull(request); @@ -246,11 +239,9 @@ public class RuleRunnerTests : IClassFixture, IClassFixture, IClassFixture x.Method == "POST" && x.Content.Contains("logo-squared", StringComparison.OrdinalIgnoreCase)); + var request = await webhookCatcher.PollAsync(sessionId, x => x.IsPost() && x.HasContent(asset.FileName)); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + AssertRequest(request); // STEP 4: Get events. @@ -278,6 +266,45 @@ public class RuleRunnerTests : IClassFixture, IClassFixture x.Id == asset.Id && x.Metadata.ContainsKey("blurHash")); + + Assert.NotNull(found); + } + [Fact] public async Task Should_run_rules_on_schema_change() { @@ -294,11 +321,9 @@ public class RuleRunnerTests : IClassFixture, IClassFixture, IClassFixture x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + var request = await webhookCatcher.PollAsync(sessionId, x => x.IsPost() && x.HasContent(schemaName)); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + AssertRequest(request); // STEP 4: Get events. @@ -342,11 +364,9 @@ public class RuleRunnerTests : IClassFixture, IClassFixture, IClassFixture x.Method == "POST"); + var request = await webhookCatcher.PollAsync(sessionId, x => x.IsPost()); - Assert.NotNull(request); - Assert.NotNull(request.Headers["X-Signature"]); - Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); + AssertRequest(request); // STEP 4: Get events. @@ -392,10 +409,9 @@ public class RuleRunnerTests : IClassFixture, IClassFixture, IClassFixture x.IsPost() && x.HasContent(schemaName)); - Assert.Contains(requests, x => x.Method == "POST" && x.Content.Contains(schemaName, StringComparison.OrdinalIgnoreCase)); + AssertRequest(request); } private async Task CreateContentAsync(ISquidexClient app) @@ -435,7 +451,7 @@ public class RuleRunnerTests : IClassFixture, IClassFixture CreateAssetAsync(ISquidexClient app) { // Upload a test asset var fileInfo = new FileInfo("Assets/logo-squared.png"); @@ -444,7 +460,15 @@ public class RuleRunnerTests : IClassFixture, IClassFixture // STEP 2: Search for schema. - var result = await _.Client.Search.WaitForSearchAsync(schemaName, x => x.Type == SearchResultType.Content, TimeSpan.FromSeconds(30)); + var result = await _.Client.Search.PollAsync(schemaName, x => x.Type == SearchResultType.Content); - Assert.NotEmpty(result); + Assert.NotNull(result); } [Fact] @@ -70,9 +70,9 @@ public class SearchTests : IClassFixture // STEP 2: Search for schema. - var result = await _.Client.Search.WaitForSearchAsync(contentString, x => x.Type == SearchResultType.Content, TimeSpan.FromSeconds(30)); + var result = await _.Client.Search.PollAsync(contentString, x => x.Type == SearchResultType.Content); - Assert.NotEmpty(result); + Assert.NotNull(result); } [Theory] diff --git a/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj index 81e425730..334b241f7 100644 --- a/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj +++ b/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj @@ -2,7 +2,7 @@ Exe net7.0 - 10.0 + 11.0 enable diff --git a/tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_compute_blur_hash_script.verified.txt b/tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_compute_blur_hash_script.verified.txt new file mode 100644 index 000000000..ff8cec307 --- /dev/null +++ b/tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_compute_blur_hash_script.verified.txt @@ -0,0 +1,49 @@ +{ + Id: Guid_1, + ParentId: Guid_2, + FileName: logo-squared.png, + FileHash: zWbRIMB2AYcz3lddAtaNFB4//qDyuwTwNKuUgKHp0Vc=, + IsProtected: false, + Slug: logo-squared.png, + MimeType: image/png, + FileType: png, + MetadataText: 600x135px, 17.1 kB, + Metadata: { + blurHash: UH2%3Akvb}kufmflj]j^RLaIjDe,f4f5axf4, + description: PNG File, + pixelHeight: 135, + pixelWidth: 600 + }, + Tags: [ + type/png, + image, + image/medium + ], + FileSize: 17539, + FileVersion: 1, + Type: Image, + Version: 1, + Links: { + content: { + Method: GET + }, + content/slug: { + Method: GET + }, + delete: { + Method: DELETE + }, + move: { + Method: PUT + }, + self: { + Method: GET + }, + update: { + Method: PUT + }, + upload: { + Method: PUT + } + } +} \ No newline at end of file diff --git a/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj b/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj index 7ce949426..dff49dbf7 100644 --- a/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj +++ b/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj @@ -2,7 +2,7 @@ Exe net7.0 - 10.0 + 11.0 enable diff --git a/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs b/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs index a2dce3c5a..20ad2d830 100644 --- a/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs +++ b/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs @@ -12,17 +12,40 @@ namespace TestSuite; public static class ClientExtensions { - public static async Task WaitForDeletionAsync(this IAssetsClient assetsClient, string id, TimeSpan timeout) + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30); + + private static TimeSpan GetTimeout(TimeSpan timeout) + { + if (timeout == default) + { + return DefaultTimeout; + } + + return timeout; + } + + public static bool IsPost(this WebhookRequest request) + { + return request.Method == "POST"; + } + + public static bool HasContent(this WebhookRequest request, string content) + { + return request.Content.Contains(content, StringComparison.OrdinalIgnoreCase); + } + + public static async Task PollForDeletionAsync(this IAssetsClient client, string id, + TimeSpan timeout = default) { try { - using var cts = new CancellationTokenSource(timeout); + using var cts = new CancellationTokenSource(GetTimeout(timeout)); while (!cts.IsCancellationRequested) { try { - await assetsClient.GetAssetAsync(id, cts.Token); + await client.GetAssetAsync(id, cts.Token); } catch (SquidexException ex) when (ex.StatusCode == 404) { @@ -39,15 +62,43 @@ public static class ClientExtensions return false; } - public static async Task> WaitForContentAsync(this IContentsClient contentsClient, ContentQuery q, Func predicate, TimeSpan timeout) where TEntity : Content where TData : class, new() + public static async Task PollAsync(this IAssetsClient client, Func predicate, + TimeSpan timeout = default) { try { - using var cts = new CancellationTokenSource(timeout); + using var cts = new CancellationTokenSource(GetTimeout(timeout)); + + while (!cts.IsCancellationRequested) + { + var results = await client.GetAssetsAsync(null, cts.Token); + var result = results.Items.FirstOrDefault(predicate); + + if (result != null) + { + return result; + } + + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { + } + + return null; + } + + public static async Task> PollAsync(this IContentsClient client, ContentQuery q, Func predicate, + TimeSpan timeout = default) where TEntity : Content where TData : class, new() + { + try + { + using var cts = new CancellationTokenSource(GetTimeout(timeout)); while (!cts.IsCancellationRequested) { - var result = await contentsClient.GetAsync(q, null, cts.Token); + var result = await client.GetAsync(q, null, cts.Token); if (result.Items.Any(predicate)) { @@ -64,19 +115,21 @@ public static class ClientExtensions return new ContentsResult(); } - public static async Task> WaitForSearchAsync(this ISearchClient searchClient, string query, Func predicate, TimeSpan timeout) + public static async Task PollAsync(this ISearchClient client, string query, Func predicate, + TimeSpan timeout = default) { try { - using var cts = new CancellationTokenSource(timeout); + using var cts = new CancellationTokenSource(GetTimeout(timeout)); while (!cts.IsCancellationRequested) { - var result = await searchClient.GetSearchResultsAsync(query, cts.Token); + var results = await client.GetSearchResultsAsync(query, cts.Token); + var result = results.FirstOrDefault(predicate); - if (result.Any(predicate)) + if (result != null) { - return result.ToList(); + return result; } await Task.Delay(200, cts.Token); @@ -86,45 +139,54 @@ public static class ClientExtensions { } - return new List(); + return null; } - public static async Task> WaitForHistoryAsync(this IHistoryClient historyClient, string channel, Func predicate, TimeSpan timeout) + public static async Task PollAsync(this WebhookCatcherClient client, string sessionId, Func predicate, + TimeSpan timeout = default) { + if (timeout == default) + { + timeout = TimeSpan.FromMinutes(2); + } + try { using var cts = new CancellationTokenSource(timeout); while (!cts.IsCancellationRequested) { - var result = await historyClient.GetAppHistoryAsync(channel, cts.Token); + var results = await client.GetRequestsAsync(sessionId, cts.Token); + var result = results.FirstOrDefault(predicate); - if (result.Any(predicate)) + if (result != null) { - return result.ToList(); + return result; } - await Task.Delay(200, cts.Token); + await Task.Delay(50, cts.Token); } } catch (OperationCanceledException) { } - return new List(); + return null; } - public static async Task> WaitForTagsAsync(this IAssetsClient assetsClient, string id, TimeSpan timeout) + public static async Task PollAsync(this IHistoryClient client, string channel, Func predicate, + TimeSpan timeout = default) { try { - using var cts = new CancellationTokenSource(timeout); + using var cts = new CancellationTokenSource(GetTimeout(timeout)); while (!cts.IsCancellationRequested) { - var result = await assetsClient.GetTagsAsync(cts.Token); + var results = await client.GetAppHistoryAsync(channel, cts.Token); + var result = results.FirstOrDefault(predicate); - if (result.TryGetValue(id, out var count) && count > 0) + if (result != null) { return result; } @@ -136,22 +198,24 @@ public static class ClientExtensions { } - return await assetsClient.GetTagsAsync(); + return null; } - public static async Task> WaitForBackupsAsync(this IBackupsClient backupsClient, Func predicate, TimeSpan timeout) + public static async Task PollAsync(this IBackupsClient client, Func predicate, + TimeSpan timeout = default) { try { - using var cts = new CancellationTokenSource(timeout); + using var cts = new CancellationTokenSource(GetTimeout(timeout)); while (!cts.IsCancellationRequested) { - var result = await backupsClient.GetBackupsAsync(cts.Token); + var results = await client.GetBackupsAsync(cts.Token); + var result = results.Items.FirstOrDefault(predicate); - if (result.Items.Any(predicate)) + if (result != null) { - return result.Items; + return result; } await Task.Delay(200, cts.Token); @@ -164,15 +228,42 @@ public static class ClientExtensions return null; } - public static async Task WaitForRestoreAsync(this IBackupsClient backupsClient, Func predicate, TimeSpan timeout) + public static async Task> PollTagAsync(this IAssetsClient client, string id, + TimeSpan timeout = default) { try { - using var cts = new CancellationTokenSource(timeout); + using var cts = new CancellationTokenSource(GetTimeout(timeout)); while (!cts.IsCancellationRequested) { - var result = await backupsClient.GetRestoreJobAsync(cts.Token); + var result = await client.GetTagsAsync(cts.Token); + + if (result.TryGetValue(id, out var count) && count > 0) + { + return result; + } + + await Task.Delay(200, cts.Token); + } + } + catch (OperationCanceledException) + { + } + + return await client.GetTagsAsync(); + } + + public static async Task PollRestoreAsync(this IBackupsClient client, Func predicate, + TimeSpan timeout = default) + { + try + { + using var cts = new CancellationTokenSource(GetTimeout(timeout)); + + while (!cts.IsCancellationRequested) + { + var result = await client.GetRestoreJobAsync(cts.Token); if (predicate(result)) { @@ -242,6 +333,18 @@ public static class ClientExtensions } } + public static async Task UpdateFileAsync(this IAssetsClient assetsClients, string id, string path, string fileType, string fileName = null) + { + var fileInfo = new FileInfo(path); + + await using (var stream = fileInfo.OpenRead()) + { + var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType); + + return await assetsClients.PutAssetContentAsync(id, upload); + } + } + public static async Task UploadRandomFileAsync(this IAssetsClient assetsClients, int size, string parentId = null, string id = null) { using (var stream = RandomAsset(size)) diff --git a/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs b/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs index af58e9222..447ef38a6 100644 --- a/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs +++ b/tools/TestSuite/TestSuite.Shared/Fixtures/WebhookCatcherClient.cs @@ -109,31 +109,4 @@ public sealed class WebhookCatcherClient return result; } - - public async Task WaitForRequestsAsync(string sessionId, TimeSpan timeout) - { - var requests = Array.Empty(); - - try - { - using var cts = new CancellationTokenSource(timeout); - - while (!cts.IsCancellationRequested) - { - requests = await GetRequestsAsync(sessionId, cts.Token); - - if (requests.Length > 0) - { - break; - } - - await Task.Delay(50, cts.Token); - } - } - catch (OperationCanceledException) - { - } - - return requests; - } } diff --git a/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index 350722046..62548a879 100644 --- a/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -2,7 +2,7 @@ net7.0 TestSuite - 10.0 + 11.0 enable diff --git a/tools/TestSuite/webhook-catcher/docker-compose.yml b/tools/TestSuite/webhook-catcher/docker-compose.yml new file mode 100644 index 000000000..54ec99e6d --- /dev/null +++ b/tools/TestSuite/webhook-catcher/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' +services: + webhookcatcher: + image: tarampampam/webhook-tester + command: serve --port 1026 + ports: + - "1026:1026" + networks: + - internal + +networks: + internal: + driver: bridge \ No newline at end of file