Browse Source

GraphQL Data loader fix. (#1006)

* GraphQL Data loader fix.

* Fix tests

* Fix event store.

* Simplify tests
pull/1009/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
6b7c6fc8a6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs
  2. 2
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  3. 2
      backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj
  4. 2
      backend/src/Migrations/Migrations.csproj
  5. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  7. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  8. 22
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs
  9. 33
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs
  10. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs
  11. 30
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs
  12. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  13. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs
  14. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs
  15. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs
  16. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs
  17. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  18. 93
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs
  20. 109
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/WritableContext.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  23. 28
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  24. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs
  25. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  27. 8
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs
  28. 109
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs
  29. 207
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  31. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs
  32. 14
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs
  33. 124
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs
  34. 41
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs
  35. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs
  36. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs
  37. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/ScriptAsset.cs
  38. 69
      backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs
  39. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  40. 70
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingBatchLoader.cs
  41. 36
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingDataLoaderExtensions.cs
  42. 25
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/EmptyDataLoaderResult.cs
  43. 34
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/NonCachingBatchLoader.cs
  44. 126
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  45. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs
  46. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs
  47. 17
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  48. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  49. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs
  50. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  51. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  52. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  53. 9
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.Designer.cs
  54. 3
      backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx
  55. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  56. 2
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  57. 2
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  58. 2
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  59. 2
      backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  60. 2
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  61. 5
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs
  62. 4
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
  63. 4
      backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs
  64. 2
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
  65. 2
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  66. 8
      backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs
  67. 79
      backend/src/Squidex.Infrastructure/Caching/QueryCache.cs
  68. 125
      backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs
  69. 125
      backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs
  70. 273
      backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs
  71. 4
      backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs
  72. 2
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  73. 2
      backend/src/Squidex.Web/Pipeline/SchemaResolver.cs
  74. 2
      backend/src/Squidex.Web/Squidex.Web.csproj
  75. 8
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs
  76. 12
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  77. 8
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs
  78. 2
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  79. 8
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  80. 2
      backend/src/Squidex/Config/Domain/AssetServices.cs
  81. 2
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  82. 11
      backend/src/Squidex/Config/Web/WebServices.cs
  83. 2
      backend/src/Squidex/Squidex.csproj
  84. 3
      backend/src/Squidex/appsettings.json
  85. 11
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/AssetMetadataWrapperTests.cs
  86. 5
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  87. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs
  88. 13
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs
  89. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  90. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs
  91. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs
  92. 62
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  93. 13
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs
  94. 24
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs
  95. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  96. 2
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  97. 164
      backend/tests/Squidex.Infrastructure.Tests/Caching/QueryCacheTests.cs
  98. 478
      backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs
  99. 2
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  100. 2
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj

22
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<ScriptAction, Script
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
Event = job.Event
Event = job.Event,
AppId = job.Event.AppId.Id,
AppName = job.Event.AppId.Name,
};
if (job.Event is EnrichedUserEventBase userEvent)
{
vars.User = AllPrinicpal();
}
var result = await scriptEngine.ExecuteAsync(vars, job.Script, ct: ct);
return Result.Success(result.ToString());
}
private static ClaimsPrincipal AllPrinicpal()
{
var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.All));
return claimsPrincipal;
}
}
public sealed class ScriptJob

2
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

2
backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj

@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>NU1608</NoWarn>
</PropertyGroup>

2
backend/src/Migrations/Migrations.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

2
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

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

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
<Nullable>enable</Nullable>

12
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(@"^(?<FullPath>(?<Type>[^_]*)_(?<Path>[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
private static readonly Regex RegexPatternNew = new Regex(@"^\{(?<FullPath>(?<Type>[\w]+)_(?<Path>[\w\.\-]+))[\s]*(\|[\s]*(?<Transform>[^\?}]+))?(\?[\s]*(?<Fallback>[^\}\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<IRuleEventFormatter> formatters;
private readonly ITemplateEngine templateEngine;
@ -392,4 +392,10 @@ public class RuleEventFormatter
return false;
}
[GeneratedRegex(@"^(?<FullPath>(?<Type>[^_]*)_(?<Path>[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture)]
private static partial Regex RegexPatternOldFactory();
[GeneratedRegex(@"^\{(?<FullPath>(?<Type>[\w]+)_(?<Path>[\w\.\-]+))[\s]*(\|[\s]*(?<Transform>[^\?}]+))?(\?[\s]*(?<Fallback>[^\}\s]+))?[\s]*\}", RegexOptions.Compiled | RegexOptions.ExplicitCapture)]
private static partial Regex RegexPatternNewFactory();
}

22
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<string>? 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);
}
}

33
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<string, JsonValue>(value) : null);
set => SetInitial(value != null ? new ReadOnlyDictionary<string, JsonValue>(value) : null);
}
[FieldDescription(nameof(FieldDescriptions.AssetTags))]
public HashSet<string>? Tags
{
set => SetValue(value != null ? new ReadOnlyCollection<string>(value.ToList()) : null);
set => SetInitial(value != null ? new ReadOnlyCollection<string>(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);
}
}

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

30
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<ContentData?>();
set => SetValue(value);
set => SetInitial(value);
}
}

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

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

@ -14,6 +14,6 @@ public class DataScriptVars : ScriptVars
public virtual ContentData? Data
{
get => GetValue<ContentData?>();
set => SetValue(value);
set => SetInitial(value);
}
}

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

2
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<string, object?>
public sealed class AssetMetadataWrapper : IDictionary<string, object?>
{
private readonly AssetMetadata metadata;

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

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

93
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs

@ -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<KeyValuePair<string, (object? Value, bool IsReadonly)>>
{
private readonly Dictionary<string, (object? Value, bool IsReadonly)> values = new Dictionary<string, (object? Value, bool IsReadonly)>(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<T>(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<KeyValuePair<string, (object? Value, bool IsReadonly)>> GetEnumerator()
{
return values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return values.GetEnumerator();
}
}

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

109
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<string, object?>
{
private readonly Dictionary<string, object?> values = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> lockedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public IEnumerable<string> Keys
{
get => values.Keys;
}
public IEnumerable<object?> 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<T>(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<T>([CallerMemberName] string? key = null)
@ -35,4 +123,19 @@ public class ScriptVars : ScriptContext
return default!;
}
public bool ContainsKey(string key)
{
return values.ContainsKey(key);
}
IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator()
{
return values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return values.GetEnumerator();
}
}

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

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

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
<Nullable>enable</Nullable>
@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Fluid.Core" Version="2.4.0" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.0.0-beta-2046" />
<PackageReference Include="Jint" Version="3.0.0-beta-2049" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

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

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs

@ -27,8 +27,9 @@ public sealed partial class MongoAssetFolderRepository : ISnapshotStore<AssetFol
IAsyncEnumerable<SnapshotResult<AssetFolderDomainObject.State>> ISnapshotStore<AssetFolderDomainObject.State>.ReadAllAsync(
CancellationToken ct)
{
return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct)
.Select(x => new SnapshotResult<AssetFolderDomainObject.State>(x.DocumentId, x.ToState(), x.Version, true));
var documents = Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct);
return documents.Select(x => new SnapshotResult<AssetFolderDomainObject.State>(x.DocumentId, x.ToState(), x.Version, true));
}
async Task<SnapshotResult<AssetFolderDomainObject.State>> ISnapshotStore<AssetFolderDomainObject.State>.ReadAsync(DomainId key,

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs

@ -27,8 +27,9 @@ public sealed partial class MongoAssetRepository : ISnapshotStore<AssetDomainObj
IAsyncEnumerable<SnapshotResult<AssetDomainObject.State>> ISnapshotStore<AssetDomainObject.State>.ReadAllAsync(
CancellationToken ct)
{
return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct)
.Select(x => new SnapshotResult<AssetDomainObject.State>(x.DocumentId, x.ToState(), x.Version));
var documents = Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct);
return documents.Select(x => new SnapshotResult<AssetDomainObject.State>(x.DocumentId, x.ToState(), x.Version));
}
async Task<SnapshotResult<AssetDomainObject.State>> ISnapshotStore<AssetDomainObject.State>.ReadAsync(DomainId key,

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

8
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<AppCommandBase, AppDomainObject>
{
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<AppCommand
await using (var uploadStream = file.OpenRead())
{
var image = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, file.MimeType, ct);
var image = await assetGenerator.GetImageInfoAsync(uploadStream, file.MimeType, ct);
if (image == null)
{

109
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs

@ -11,7 +11,6 @@ using Fluid.Ast;
using Fluid.Values;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Infrastructure;
@ -21,9 +20,6 @@ namespace Squidex.Domain.Apps.Entities.Assets;
public sealed class AssetsFluidExtension : IFluidExtension
{
private static readonly FluidValue ErrorNoAsset = new StringValue("NoAsset");
private static readonly FluidValue ErrorNoImage = new StringValue("NoImage");
private static readonly FluidValue ErrorTooBig = new StringValue("ErrorTooBig");
private readonly IServiceProvider serviceProvider;
public AssetsFluidExtension(IServiceProvider serviceProvider)
@ -85,91 +81,60 @@ public sealed class AssetsFluidExtension : IFluidExtension
{
options.Filters.AddFilter("assetText", async (input, arguments, context) =>
{
if (input is not ObjectValue objectValue)
{
return ErrorNoAsset;
}
async Task<FluidValue> ResolveAssetTextAsync(AssetRef asset)
{
if (asset.FileSize > 256_000)
{
return ErrorTooBig;
}
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
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<FluidValue> 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<IAssetFileStore>();
var assetThumbnailGenerator = serviceProvider.GetRequiredService<IAssetThumbnailGenerator>();
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<IAssetEntity?> ResolveAssetAsync(IServiceProvider serviceProvider, DomainId appId, FluidValue id)

207
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<JsValue> callback);
private delegate void GetAssetTextDelegate(JsValue asset, Action<JsValue> callback, JsValue? encoding);
private delegate void GetBlurHashDelegate(JsValue asset, Action<JsValue> 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<ClaimsPrincipal>("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<ICommandBus>();
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<DomainId>("appId", out var appId))
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>("user", out var user))
if (!context.TryGetValueIfExists<ClaimsPrincipal>("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<IAssetFileStore>();
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<IAssetThumbnailGenerator>();
var assetFileStore = serviceProvider.GetRequiredService<IAssetFileStore>();
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<string>(nameof(AssetScriptVars.AppName), out var appName) ||
!context.TryGetValueIfExists<DomainId>(nameof(AssetScriptVars.AppId), out var appId) ||
!context.TryGetValueIfExists<DomainId>(nameof(AssetScriptVars.AssetId), out var assetId))
{
return false;
}
context.TryGetValueIfExists<string?>(nameof(AssetScriptVars.FileId), out var fileId);
assetRef = new AssetRef(
NamedId.Of(appId, appName),
assetId,
vars.GetValue<long>(nameof(AssetEntityScriptVars.FileVersion)),
vars.GetValue<long>(nameof(AssetEntityScriptVars.FileSize)),
vars.GetValue<string>(nameof(AssetEntityScriptVars.MimeType)),
fileId,
vars.GetValue<AssetType>(nameof(AssetEntityScriptVars.Type)));
return true;
}
return true;
}
private async Task<IAppEntity> 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);
}
}

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

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

14
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs

@ -69,11 +69,12 @@ public sealed class AssetCommandMiddleware : CachingDomainObjectMiddleware<Asset
private async Task UploadWithDuplicateCheckAsync(CommandContext context, UploadAssetCommand command, bool duplicate, NextDelegate next,
CancellationToken ct)
{
var tempFile = context.ContextId.ToString();
// Add the file Id to the command, so we can access it later in the script.
command.FileId = context.ContextId.ToString();
try
{
await EnrichWithHashAndUploadAsync(command, tempFile, ct);
await EnrichWithHashAndUploadAsync(command, command.FileId, ct);
if (!duplicate)
{
@ -99,25 +100,26 @@ public sealed class AssetCommandMiddleware : CachingDomainObjectMiddleware<Asset
}
finally
{
await assetFileStore.DeleteAsync(tempFile, ct);
await assetFileStore.DeleteAsync(command.FileId, ct);
}
}
private async Task UploadAndHandleAsync(CommandContext context, UploadAssetCommand command, NextDelegate next,
CancellationToken ct)
{
var tempFile = context.ContextId.ToString();
// Add the file Id to the command, so we can access it later in the script.
command.FileId = context.ContextId.ToString();
try
{
await EnrichWithHashAndUploadAsync(command, tempFile, ct);
await EnrichWithHashAndUploadAsync(command, command.FileId, ct);
await EnrichWithMetadataAsync(command, ct);
await base.HandleAsync(context, next, ct);
}
finally
{
await assetFileStore.DeleteAsync(tempFile, ct);
await assetFileStore.DeleteAsync(command.FileId, ct);
}
}

124
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs

@ -1,124 +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.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards;
public sealed class ScriptMetadataWrapper : IDictionary<string, object?>
{
private readonly AssetMetadata metadata;
public int Count
{
get => metadata.Count;
}
public ICollection<string> Keys
{
get => metadata.Keys;
}
public ICollection<object?> Values
{
get => metadata.Values.Cast<object?>().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<string, object?> item)
{
Add(item.Key, item.Value);
}
public bool Remove(string key)
{
return metadata.Remove(key);
}
public bool Remove(KeyValuePair<string, object?> item)
{
return false;
}
public void Clear()
{
metadata.Clear();
}
public bool Contains(KeyValuePair<string, object?> item)
{
return false;
}
public bool ContainsKey(string key)
{
return metadata.ContainsKey(key);
}
public void CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex)
{
var i = arrayIndex;
foreach (var item in metadata)
{
if (i >= array.Length)
{
break;
}
array[i] = new KeyValuePair<string, object?>(item.Key, item.Value);
i++;
}
}
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
{
return metadata.Select(x => new KeyValuePair<string, object?>(x.Key, x.Value)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)metadata).GetEnumerator();
}
}

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

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

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

3
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,

69
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<DomainId> 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<string> GetTextAsync(this AssetRef asset, string? encoding,
IAssetFileStore assetFileStore,
public static async Task<string> 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<IAssetFileStore>();
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<string?> GetBlurHashAsync(this AssetRef asset, BlurOptions options,
IAssetFileStore assetFileStore,
IAssetThumbnailGenerator assetThumbnails,
public static async Task<string?> 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<IAssetFileStore>();
var assetGenerator = services.GetRequiredService<IAssetThumbnailGenerator>();
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;
}
}

4
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<DomainId>("appId", out var appId))
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
}
@ -48,7 +48,7 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
public void ExtendAsync(ScriptExecutionContext context)
{
if (!context.TryGetValue<DomainId>("appId", out var appId))
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
}

70
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>(T Id, TimeSpan CacheDuration = default);
internal class CachingBatchDataLoader<TKey, T> : DataLoaderBase<CacheableId<TKey>, T> where TKey : notnull where T : class
{
private readonly IQueryCache<TKey, T> queryCache;
private readonly Func<IEnumerable<TKey>, CancellationToken, Task<IDictionary<TKey, T>>> queryDelegate;
public CachingBatchDataLoader(IQueryCache<TKey, T> queryStore,
Func<IEnumerable<TKey>, CancellationToken, Task<IDictionary<TKey, T>>> queryDelegate, bool canCache = true, int maxBatchSize = int.MaxValue)
: base(canCache, maxBatchSize)
{
this.queryCache = queryStore;
this.queryDelegate = queryDelegate;
}
protected override async Task FetchAsync(IEnumerable<DataLoaderPair<CacheableId<TKey>, T>> list,
CancellationToken cancellationToken)
{
var unmatched = new List<DataLoaderPair<CacheableId<TKey>, 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);
}
}
}
}

36
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<CacheableId<TKey>, T> GetOrAddCachingLoader<TKey, T>(this DataLoaderContext dataLoaderContext, IQueryCache<TKey, T> queryCache, string loaderKey,
Func<IEnumerable<TKey>, CancellationToken, Task<IDictionary<TKey, T>>> queryDelegate, bool canCache = true, int maxBatchSize = int.MaxValue)
where TKey : notnull where T : class
{
return dataLoaderContext.GetOrAdd(loaderKey, () =>
{
return new CachingBatchDataLoader<TKey, T>(queryCache, queryDelegate, canCache, maxBatchSize);
});
}
public static IDataLoader<TKey, T> GetOrAddNonCachingBatchLoader<TKey, T>(this DataLoaderContext dataLoaderContext, string loaderKey,
Func<IEnumerable<TKey>, CancellationToken, Task<IDictionary<TKey, T>>> queryDelegate, int maxBatchSize = int.MaxValue)
where TKey : notnull where T : class
{
return dataLoaderContext.GetOrAdd(loaderKey, () =>
{
return new NonCachingBatchLoader<TKey, T>(queryDelegate, maxBatchSize);
});
}
}

25
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<T> : IDataLoaderResult<T[]>
{
public Task<T[]> GetResultAsync(
CancellationToken cancellationToken = default)
{
return Task.FromResult(Array.Empty<T>());
}
Task<object?> IDataLoaderResult.GetResultAsync(
CancellationToken cancellationToken)
{
return Task.FromResult<object?>(Array.Empty<T>());
}
}

34
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<TKey, T> : DataLoaderBase<TKey, T> where TKey : notnull where T : class
{
private readonly Func<IEnumerable<TKey>, CancellationToken, Task<IDictionary<TKey, T>>> queryDelegate;
public NonCachingBatchLoader(Func<IEnumerable<TKey>, CancellationToken, Task<IDictionary<TKey, T>>> queryDelegate, int maxBatchSize = int.MaxValue)
: base(false, maxBatchSize)
{
this.queryDelegate = queryDelegate;
}
protected override async Task FetchAsync(IEnumerable<DataLoaderPair<TKey, T>> 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!);
}
}
}

126
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<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>();
private static readonly List<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>();
private static readonly EmptyDataLoaderResult<IEnrichedAssetEntity> EmptyAssets = new EmptyDataLoaderResult<IEnrichedAssetEntity>();
private static readonly EmptyDataLoaderResult<IEnrichedContentEntity> EmptyContents = new EmptyDataLoaderResult<IEnrichedContentEntity>();
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<GraphQLOptions> 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<IUser?> FindUserAsync(RefToken refToken,
@ -56,104 +60,84 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
}
}
public async Task<IEnrichedAssetEntity?> GetAssetAsync(DomainId id, TimeSpan cacheDuration,
CancellationToken ct)
public IDataLoaderResult<IEnrichedContentEntity?> GetContent(DomainId schemaId, DomainId id, long version)
{
var assets = await GetAssetsAsync(new List<DomainId> { id }, cacheDuration, ct);
var asset = assets.FirstOrDefault();
return dataLoaders.Context!.GetOrAddLoader(nameof(GetContent), ct =>
{
return FindContentAsync(schemaId.ToString(), id, version, ct);
}).LoadAsync();
}
public IDataLoaderResult<IEnrichedAssetEntity?> GetAsset(DomainId id,
TimeSpan cacheDuration)
{
var assets = GetAssets(new List<DomainId> { id }, cacheDuration);
var asset = assets.Then(x => x.FirstOrDefault());
return asset;
}
public async Task<IEnrichedContentEntity?> GetContentAsync(DomainId schemaId, DomainId id, HashSet<string>? fields, TimeSpan cacheDuration,
CancellationToken ct)
public IDataLoaderResult<IEnrichedContentEntity?> GetContent(DomainId schemaId, DomainId id, HashSet<string>? fields,
TimeSpan cacheDuration)
{
var contents = await GetContentsAsync(new List<DomainId> { id }, fields, cacheDuration, ct);
var content = contents.FirstOrDefault(x => x.SchemaId.Id == schemaId);
var contents = GetContents(new List<DomainId> { id }, fields, cacheDuration);
var content = contents.Then(x => x.FirstOrDefault(x => x.SchemaId.Id == schemaId));
return content;
}
public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetAssetsAsync(List<DomainId>? ids, TimeSpan cacheDuration,
CancellationToken ct)
public IDataLoaderResult<IEnrichedAssetEntity[]> GetAssets(List<DomainId>? ids,
TimeSpan cacheDuration)
{
if (ids == null || ids.Count == 0)
{
return EmptyAssets;
}
async Task<IReadOnlyList<IEnrichedAssetEntity>> LoadAsync(IEnumerable<DomainId> 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<IReadOnlyList<IEnrichedContentEntity>> GetContentsAsync(List<DomainId>? ids, HashSet<string>? fields, TimeSpan cacheDuration,
CancellationToken ct)
public IDataLoaderResult<IEnrichedContentEntity[]> GetContents(List<DomainId>? ids, HashSet<string>? 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<DomainId, IEnrichedAssetEntity> GetAssetsLoader()
private IDataLoader<CacheableId<DomainId>, IEnrichedAssetEntity> GetAssetsLoader()
{
return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedAssetEntity>(nameof(GetAssetsLoader),
return dataLoaders.Context!.GetOrAddCachingLoader(AssetCache, nameof(GetAssetsLoader),
async (batch, ct) =>
{
var result = await QueryAssetsByIdsAsync(new List<DomainId>(batch), ct);
var result = await QueryAssetsByIdsAsync(batch, ct);
return result.ToDictionary(x => x.Id);
});
}, maxBatchSize: options.DataLoaderBatchSize);
}
private IDataLoader<DomainId, IEnrichedContentEntity> GetContentsLoader()
private IDataLoader<CacheableId<DomainId>, IEnrichedContentEntity> GetContentsLoader()
{
return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedContentEntity>(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<string> Fields), IEnrichedContentEntity> GetContentsLoaderWithFields()
{
return dataLoaders.Context!.GetOrAddBatchLoader<(DomainId Id, HashSet<string> Fields), IEnrichedContentEntity>(nameof(GetContentsLoader),
return dataLoaders.Context!.GetOrAddNonCachingBatchLoader<(DomainId Id, HashSet<string> 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<string, IUser> GetUserLoader()
@ -174,4 +158,30 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
return result;
});
}
private static (DomainId, HashSet<string>)[] BuildKeys(List<DomainId> ids, HashSet<string> fields)
{
// Use manual loops and arrays to avoid allocations.
var keys = new (DomainId, HashSet<string>)[ids.Count];
for (var i = 0; i < ids.Count; i++)
{
keys[i] = (ids[0], fields);
}
return keys;
}
private static CacheableId<DomainId>[] BuildKeys(List<DomainId> ids, TimeSpan cacheDuration)
{
// Use manual loops and arrays to avoid allocations.
var keys = new CacheableId<DomainId>[ids.Count];
for (var i = 0; i < ids.Count; i++)
{
keys[i] = new CacheableId<DomainId>(ids[i], cacheDuration);
}
return keys;
}
}

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

7
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<object, object?>(async (_, fieldContext, context) =>
public static readonly IFieldResolver Resolver = Resolvers.Sync<object, object?>((_, fieldContext, context) =>
{
var assetId = fieldContext.GetArgument<DomainId>("id");
return await context.GetAssetAsync(assetId,
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
return context.GetAsset(assetId,
fieldContext.CacheDuration());
});
}

17
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<object, object?>(async (_, fieldContext, context) =>
public static readonly IFieldResolver Resolver = Resolvers.Sync<object, object?>((_, fieldContext, context) =>
{
var contentId = fieldContext.GetArgument<DomainId>("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<object, object?>(async (_, fieldContext, context) =>
public static readonly IFieldResolver Resolver = Resolvers.Sync<object, object?>((_, fieldContext, context) =>
{
var ids = fieldContext.GetArgument<DomainId[]>("ids").ToList();
return await context.GetContentsAsync(ids,
return context.GetContents(ids,
fieldContext.FieldNames(),
fieldContext.CacheDuration(),
fieldContext.CancellationToken);
fieldContext.CacheDuration());
});
}

14
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<string, object>(async (value, fieldContext, context) =>
public static readonly IFieldResolver ResolveStringFieldAssets = Resolvers.Sync<string, object>((value, fieldContext, context) =>
{
var ids = context.Resolve<StringReferenceExtractor>().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<string, object>(async (value, fieldContext, context) =>
public static readonly IFieldResolver ResolveStringFieldContents = Resolvers.Sync<string, object>((value, fieldContext, context) =>
{
var ids = context.Resolve<StringReferenceExtractor>().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

14
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs

@ -98,23 +98,21 @@ internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo>
}
});
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;

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

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -65,8 +65,6 @@ public abstract class QueryExecutionContext : Dictionary<string, object?>
maxRequests.Release();
}
AssetCache.SetMany(assets.Select(x => (x.Id, x))!);
return assets;
}
@ -85,8 +83,6 @@ public abstract class QueryExecutionContext : Dictionary<string, object?>
maxRequests.Release();
}
ContentCache.SetMany(contents.Select(x => (x.Id, x))!);
return contents;
}

4
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<DomainId>("appId", out var appId))
if (!context.TryGetValueIfExists<DomainId>("appId", out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>("user", out var user))
if (!context.TryGetValueIfExists<ClaimsPrincipal>("user", out var user))
{
return;
}

9
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);
}
}
/// <summary>
/// Looks up a localized string similar to Update the metadata of the asset..
/// </summary>
internal static string ScriptingUpdateAsset {
get {
return ResourceManager.GetString("ScriptingUpdateAsset", resourceCulture);
}
}
}
}

3
backend/src/Squidex.Domain.Apps.Entities/Properties/Resources.resx

@ -159,4 +159,7 @@
<data name="ScriptingResetCounterV2" xml:space="preserve">
<value>Resets the counter with the given name to zero.</value>
</data>
<data name="ScriptingUpdateAsset" xml:space="preserve">
<value>Update the metadata of the asset.</value>
</data>
</root>

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
<Nullable>enable</Nullable>

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

5
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs

@ -143,10 +143,9 @@ public sealed class MongoEventStoreSubscription : IEventSubscription
if (byStream != null)
{
var filterBuilder = Builders<ChangeStreamDocument<MongoEventCommit>>.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;

4
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs

@ -166,12 +166,12 @@ public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, 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))
{

4
backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs

@ -81,6 +81,8 @@ public sealed class MongoRequestLogRepository : MongoRepositoryBase<MongoRequest
var find = Collection.Find(x => 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());
}
}

2
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs

@ -57,7 +57,7 @@ public static class MongoExtensions
public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IFindFluent<T, T> find,
[EnumeratorCancellation] CancellationToken ct = default)
{
var cursor = await find.ToCursorAsync(ct);
using var cursor = await find.ToCursorAsync(ct);
while (await cursor.MoveNextAsync(ct))
{

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

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

8
backend/src/Squidex.Infrastructure/Caching/IQueryCache.cs

@ -7,11 +7,9 @@
namespace Squidex.Infrastructure.Caching;
public interface IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey>
public interface IQueryCache<TKey, T> where TKey : notnull
{
void SetMany(IEnumerable<(TKey, T?)> results,
TimeSpan? permanentDuration = null);
void Set(TKey key, T item, TimeSpan cacheDuration);
Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query,
TimeSpan? permanentDuration = null);
bool TryGet(TKey key, out T result);
}

79
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<TKey, T> : IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey>
public class QueryCache<TKey, T> : IQueryCache<TKey, T> where TKey : notnull
{
private readonly ConcurrentDictionary<TKey, T?> entries = new ConcurrentDictionary<TKey, T?>();
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<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> 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<TKey>(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<TKey, T?> GetMany(IEnumerable<TKey> keys,
bool fromPermanentCache = false)
{
var result = new Dictionary<TKey, T?>();
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;
}
}

125
backend/src/Squidex.Infrastructure/Collections/ListDictionary.KeyCollection.cs

@ -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<TKey, TValue>
{
private sealed class KeyCollection : ICollection<TKey>
{
private readonly ListDictionary<TKey, TValue> dictionary;
public int Count
{
get => dictionary.Count;
}
public bool IsReadOnly
{
get => false;
}
public KeyCollection(ListDictionary<TKey, TValue> 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<TKey> GetEnumerator()
{
return new Enumerator(dictionary);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(dictionary);
}
private struct Enumerator : IEnumerator<TKey>, IEnumerator
{
private readonly ListDictionary<TKey, TValue> dictionary;
private int index = -1;
private TKey value = default!;
readonly TKey IEnumerator<TKey>.Current
{
get => value!;
}
readonly object IEnumerator.Current
{
get => value!;
}
public Enumerator(ListDictionary<TKey, TValue> 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;
}
}
}
}

125
backend/src/Squidex.Infrastructure/Collections/ListDictionary.ValueCollection.cs

@ -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<TKey, TValue>
{
private sealed class ValueCollection : ICollection<TValue>
{
private readonly ListDictionary<TKey, TValue> dictionary;
public int Count
{
get => dictionary.Count;
}
public bool IsReadOnly
{
get => false;
}
public ValueCollection(ListDictionary<TKey, TValue> 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<TValue> GetEnumerator()
{
return new Enumerator(dictionary);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(dictionary);
}
private struct Enumerator : IEnumerator<TValue>, IEnumerator
{
private readonly ListDictionary<TKey, TValue> dictionary;
private int index = -1;
private TValue value = default!;
readonly TValue IEnumerator<TValue>.Current
{
get => value!;
}
readonly object IEnumerator.Current
{
get => value!;
}
public Enumerator(ListDictionary<TKey, TValue> 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;
}
}
}
}

273
backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs

@ -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<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TKey : notnull
{
private readonly List<KeyValuePair<TKey, TValue>> entries = new List<KeyValuePair<TKey, TValue>>();
private readonly IEqualityComparer<TKey> comparer;
private struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>, IEnumerator
{
private readonly ListDictionary<TKey, TValue> dictionary;
private int index = -1;
private KeyValuePair<TKey, TValue> value = default!;
readonly KeyValuePair<TKey, TValue> IEnumerator<KeyValuePair<TKey, TValue>>.Current
{
get => value!;
}
readonly object IEnumerator.Current
{
get => value!;
}
public Enumerator(ListDictionary<TKey, TValue> 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<TKey, TValue>(key, value);
}
else
{
entries.Add(new KeyValuePair<TKey, TValue>(key, value));
}
}
}
public ICollection<TKey> Keys
{
get => new KeyCollection(this);
}
public ICollection<TValue> Values
{
get => new ValueCollection(this);
}
public int Count
{
get => entries.Count;
}
public int Capacity
{
get => entries.Capacity;
}
public bool IsReadOnly
{
get => false;
}
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys
{
get => new KeyCollection(this);
}
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values
{
get => new ValueCollection(this);
}
public ListDictionary()
: this(1, null)
{
}
public ListDictionary(ListDictionary<TKey, TValue> source, IEqualityComparer<TKey>? comparer = null)
{
Guard.NotNull(source);
entries = source.entries.ToList();
this.comparer = comparer ?? EqualityComparer<TKey>.Default;
}
public ListDictionary(int capacity, IEqualityComparer<TKey>? comparer = null)
{
Guard.GreaterEquals(capacity, 0);
entries = new List<KeyValuePair<TKey, TValue>>(capacity);
this.comparer = comparer ?? EqualityComparer<TKey>.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<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
public void AddUnsafe(TKey key, TValue value)
{
entries.Add(new KeyValuePair<TKey, TValue>(key, value));
}
public void Clear()
{
entries.Clear();
}
public bool Contains(KeyValuePair<TKey, TValue> 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<TKey, TValue> 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<TKey, TValue>[] array, int arrayIndex)
{
entries.CopyTo(array, arrayIndex);
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
}

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

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
<Nullable>enable</Nullable>

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

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

8
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;
}
/// <summary>
@ -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);
}
}
}

12
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;
}
/// <summary>
@ -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);
}
}
}

8
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"))

2
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -383,7 +383,7 @@ public sealed class SchemasController : ApiController
private Task<ISchemaEntity?> GetSchemaAsync(string schema)
{
if (Guid.TryParse(schema, out var guid))
if (Guid.TryParseExact(schema, "D", out var guid))
{
var schemaId = DomainId.Create(guid);

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

2
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -46,7 +46,7 @@ public static class AssetServices
services.AddTransientAs<AssetTagsDeleter>()
.As<IDeleter>();
services.AddTransientAs<AssetCache>()
services.AddSingletonAs<AssetCache>()
.As<IAssetCache>();
services.AddSingletonAs<RebuildFiles>()

2
backend/src/Squidex/Config/Domain/ContentsServices.cs

@ -41,7 +41,7 @@ public static class ContentsServices
services.AddTransientAs<CounterService>()
.As<ICounterService>().As<IDeleter>();
services.AddTransientAs<ContentCache>()
services.AddSingletonAs<ContentCache>()
.As<IContentCache>();
services.AddSingletonAs<DefaultValidatorsFactory>()

11
backend/src/Squidex/Config/Web/WebServices.cs

@ -107,6 +107,17 @@ public static class WebServices
builder.AddSchema<DummySchema>();
builder.AddSystemTextJson();
builder.AddDataLoader();
builder.ConfigureExecutionOptions(options =>
{
var logger = options.RequestServices!.GetRequiredService<ILogger<GraphQLHttpMiddleware>>();
options.UnhandledExceptionDelegate = ctx =>
{
logger.LogError(ctx.Exception, "GraphQL error in field {field}", ctx.FieldContext?.FieldAst?.Name);
return Task.CompletedTask;
};
});
});
services.AddSingletonAs<DummySchema>()

2
backend/src/Squidex/Squidex.csproj

@ -3,7 +3,7 @@
<TargetFramework>net7.0</TargetFramework>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
<NoWarn>NU1608</NoWarn>

3
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,

11
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs → 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]

5
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -537,10 +537,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
[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;

1
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",

13
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs

@ -127,6 +127,19 @@ public class ComponentFieldTests : IClassFixture<TranslationsFixture>
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()
{

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

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs

@ -18,7 +18,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase<AppDomainObject.State>
{
private readonly IDomainObjectFactory domainObjectFactory = A.Fake<IDomainObjectFactory>();
private readonly IAppImageStore appImageStore = A.Fake<IAppImageStore>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetThumbnailGenerator assetGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly AppCommandMiddleware sut;
public sealed class MyCommand : SquidexCommand
@ -32,7 +32,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase<AppDomainObject.State>
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<AppDomainObject.State>
{
var file = new NoopAssetFile();
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
A.CallTo(() => assetGenerator.GetImageInfoAsync(A<Stream>._, 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<AppDomainObject.State>
var command = new UploadAppImage { File = file };
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
A.CallTo(() => assetGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
.Returns(Task.FromResult<ImageInfo?>(null));
await Assert.ThrowsAsync<ValidationException>(() => HandleAsync(sut, command, CancellationToken));

14
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs

@ -23,7 +23,7 @@ public class AssetsFluidExtensionTests : GivenContext
{
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetThumbnailGenerator assetGenerator = A.Fake<IAssetThumbnailGenerator>();
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<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._))
A.CallTo(() => assetGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._))
.Returns(hash);
}

62
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<TranslationsFixture>
{
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetThumbnailGenerator assetGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly JintScriptEngine sut;
public AssetsJintExtensionTests()
@ -34,9 +37,10 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
var serviceProvider =
new ServiceCollection()
.AddSingleton(AppProvider)
.AddSingleton(commandBus)
.AddSingleton(assetFileStore)
.AddSingleton(assetQuery)
.AddSingleton(assetThumbnailGenerator)
.AddSingleton(assetGenerator)
.BuildServiceProvider();
var extensions = new IJintExtension[]
@ -170,7 +174,7 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
[Fact]
public async Task Should_not_resolve_text_if_too_big()
{
var (vars, _) = SetupAssetsVars(1, 1_000_000);
var (vars, _) = SetupAssetsVars(1, 10_000_000);
var expected = @"
Text: ErrorTooBig
@ -209,7 +213,10 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
var vars = new ScriptVars
{
["event"] = @event
["event"] = @event,
["appId"] = AppId.Id,
["appName"] = AppId.Name,
["user"] = new ClaimsPrincipal(),
};
var expected = @"
@ -256,7 +263,7 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
[Fact]
public async Task Should_not_resolve_blur_hash_if_too_big()
{
var (vars, assets) = SetupAssetsVars(1, 1_000_000);
var (vars, assets) = SetupAssetsVars(1, 10_000_000);
SetupBlurHash(assets[0].ToRef(), "Hash");
@ -319,7 +326,10 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
var vars = new ScriptVars
{
["event"] = @event
["event"] = @event,
["appId"] = AppId.Id,
["appName"] = AppId.Name,
["user"] = new ClaimsPrincipal(),
};
var expected = @"
@ -338,9 +348,47 @@ public class AssetsJintExtensionTests : GivenContext, IClassFixture<Translations
Assert.Equal(Cleanup(expected), Cleanup(actual));
}
[Fact]
public async Task Should_update_asset_from_event()
{
var @event = new EnrichedAssetEvent
{
Id = DomainId.NewGuid(),
AssetType = AssetType.Image,
FileVersion = 0,
FileSize = 100,
Metadata = new AssetMetadata
{
[AssetMetadata.PixelWidth] = 100,
[AssetMetadata.PixelHeight] = 50,
},
AppId = AppId
};
var vars = new ScriptVars
{
["event"] = @event,
["appId"] = AppId.Id,
["appName"] = AppId.Name,
["user"] = new ClaimsPrincipal(),
};
var script = @"
var metadata = { ...event.metadata, newValue: 42 };
updateAsset(event, metadata);
";
var actual = (await sut.ExecuteAsync(vars, script, ct: CancellationToken)).ToString();
A.CallTo(() => commandBus.PublishAsync(
A<AnnotateAsset>.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<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._))
A.CallTo(() => assetGenerator.ComputeBlurHashAsync(A<Stream>._, asset.MimeType, A<BlurOptions>._, A<CancellationToken>._))
.Returns(hash);
}

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

24
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<IAssetThumbnailGenerator>();
private readonly IAssetThumbnailGenerator assetGenerator = A.Fake<IAssetThumbnailGenerator>();
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<Stream>._, file.MimeType, CancellationToken))
A.CallTo(() => assetGenerator.GetImageInfoAsync(A<Stream>._, 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<ImageInfo?>(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<Stream>._, A<CancellationToken>._))
A.CallTo(() => assetGenerator.FixAsync(stream, file.MimeType, A<Stream>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
@ -74,10 +74,10 @@ public class ImageAssetMetadataSourceTests : GivenContext
{
var command = new CreateAsset { File = file };
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
A.CallTo(() => assetGenerator.GetImageInfoAsync(A<Stream>._, 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<Stream>._, CancellationToken))
A.CallTo(() => assetGenerator.FixAsync(stream, file.MimeType, A<Stream>._, CancellationToken))
.MustHaveHappened();
}
@ -95,10 +95,10 @@ public class ImageAssetMetadataSourceTests : GivenContext
{
var command = new CreateAsset { File = file };
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
A.CallTo(() => assetGenerator.GetImageInfoAsync(A<Stream>._, 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<Stream>._, CancellationToken))
A.CallTo(() => assetGenerator.FixAsync(stream, file.MimeType, A<Stream>._, CancellationToken))
.MustHaveHappened();
}

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

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Entities</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NeutralLanguage>en</NeutralLanguage>

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

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Users</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

164
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<int>
{
public int Id => Value;
}
[Fact]
public async Task Should_query_from_cache()
{
var sut = new QueryCache<int, CachedEntry>();
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<int, CachedEntry>();
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<int, CachedEntry>();
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<int, CachedEntry>(memoryCache);
var sut2 = new QueryCache<int, CachedEntry>(memoryCache);
var sut = new QueryCache<int, int>(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<int, CachedEntry>(memoryCache);
var sut2 = new QueryCache<int, CachedEntry>(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<int, CachedEntry>(memoryCache);
var sut2 = new QueryCache<int, CachedEntry>(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<int, CachedEntry>();
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<int, CachedEntry>(memoryCache);
var sut2 = new QueryCache<int, CachedEntry>(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<int, CachedEntry>(memoryCache);
var sut2 = new QueryCache<int, CachedEntry>(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<int, CachedEntry> sut, params int[] ids)
{
return ConfigureAsync(sut, x => true, null, ids);
}
private static async Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, Func<int, bool> predicate, TimeSpan? cacheDuration, params int[] ids)
public void Should_not_query_from_cache_if_not_configured()
{
var queried = new HashSet<int>();
var sut = new QueryCache<int, int>();
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);
}
}

478
backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs

@ -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<int, int>();
Assert.Empty(sut);
Assert.Equal(1, sut.Capacity);
}
[Fact]
public void Should_create_with_capacity()
{
var sut = new ListDictionary<int, int>(20);
Assert.Empty(sut);
Assert.Equal(20, sut.Capacity);
}
[Fact]
public void Should_create_as_copy()
{
var source = new ListDictionary<int, int>();
source.Add(1, 10);
source.Add(2, 20);
var sut = new ListDictionary<int, int>(source);
Assert.Equal(2, sut.Count);
}
[Fact]
public void Should_not_be_readonly()
{
var sut = new ListDictionary<int, int>();
Assert.False(sut.IsReadOnly);
Assert.False(sut.Keys.IsReadOnly);
Assert.False(sut.Values.IsReadOnly);
}
[Fact]
public void Should_add_item()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
Assert.Single(sut);
Assert.Equal(10, sut[1]);
}
[Fact]
public void Should_add_item_unsafe()
{
var sut = new ListDictionary<int, int>();
sut.AddUnsafe(1, 10);
Assert.Single(sut);
Assert.Equal(10, sut[1]);
}
[Fact]
public void Should_add_item_as_pair()
{
var sut = new ListDictionary<int, int>();
sut.Add(new KeyValuePair<int, int>(1, 10));
Assert.Single(sut);
Assert.Equal(10, sut[1]);
}
[Fact]
public void Should_throw_exception_if_adding_existing_key()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
Assert.Throws<ArgumentException>(() => sut.Add(1, 20));
}
[Fact]
public void Should_throw_exception_if_adding_pair_with_existing_key()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
Assert.Throws<ArgumentException>(() => sut.Add(new KeyValuePair<int, int>(1, 20)));
}
[Fact]
public void Should_set_item()
{
var sut = new ListDictionary<int, int>();
sut[1] = 10;
Assert.Single(sut);
Assert.Equal(10, sut[1]);
}
[Fact]
public void Should_override_item()
{
var sut = new ListDictionary<int, int>();
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<int, int>();
sut.Add(1, 10);
Assert.True(sut.Contains(new KeyValuePair<int, int>(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<int, int>();
sut.Add(1, 10);
Assert.False(sut.Contains(new KeyValuePair<int, int>(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<int, int>();
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<int, int>();
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<int, int>();
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<int, int>();
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<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
sut.Add(3, 30);
Assert.True(sut.Remove(new KeyValuePair<int, int>(2, 20)));
Assert.False(sut.ContainsKey(2));
}
[Fact]
public void Should_not_remove_item_if_key_not_found()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
sut.Add(3, 30);
Assert.False(sut.Remove(new KeyValuePair<int, int>(4, 40)));
}
[Fact]
public void Should_not_remove_item_if_value_not_equal()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
sut.Add(3, 30);
Assert.False(sut.Remove(new KeyValuePair<int, int>(2, 40)));
}
[Fact]
public void Should_get_value_by_method_if_found()
{
var sut = new ListDictionary<int, int>();
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<int, int>();
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<int, int>();
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<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
Assert.Throws<KeyNotFoundException>(() => sut[4]);
}
[Fact]
public void Should_loop_over_entries()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
var actual = new List<KeyValuePair<int, int>>();
foreach (var entry in sut)
{
actual.Add(entry);
}
Assert.Equal(new[]
{
new KeyValuePair<int, int>(1, 10),
new KeyValuePair<int, int>(2, 20)
}, actual.ToArray());
}
[Fact]
public void Should_loop_over_entries_with_old_enumerator()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
var actual = new List<KeyValuePair<int, int>>();
foreach (KeyValuePair<int, int> entry in (IEnumerable)sut)
{
actual.Add(entry);
}
Assert.Equal(new[]
{
new KeyValuePair<int, int>(1, 10),
new KeyValuePair<int, int>(2, 20)
}, actual.ToArray());
}
[Fact]
public void Should_copy_entries_to_array()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
Assert.Equal(new[]
{
new KeyValuePair<int, int>(1, 10),
new KeyValuePair<int, int>(2, 20)
}, sut.ToArray());
}
[Fact]
public void Should_loop_over_keys()
{
var sut = new ListDictionary<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
var actual = new List<int>();
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<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
var actual = new List<int>();
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<int, int>();
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<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
var actual = new List<int>();
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<int, int>();
sut.Add(1, 10);
sut.Add(2, 20);
var actual = new List<int>();
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<int, int>();
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<int, int>(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<int, int>
{
[11] = 1,
[12] = 2,
[13] = 3
}.ToReadonlyDictionary();
var serialized = sut.SerializeAndDeserialize();
Assert.Equal(sut, serialized);
}
}

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

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NeutralLanguage>en</NeutralLanguage>

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

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Web</RootNamespace>
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

Loading…
Cancel
Save