Browse Source

Optimize allocations (#1008)

* Improve async code.

* Improve assets.

* Fix tests.

* More tests

* Fix mapping.

* Fix header names.

* Fix languages header

* Optimize allocations.

* Improve graphql queries.
pull/1009/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
8a685d59ac
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs
  2. 34
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  3. 34
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  4. 30
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs
  5. 1
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  6. 16
      backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  8. 1
      backend/src/Squidex.Domain.Apps.Entities/Assets/Transformations.cs
  9. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs
  10. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingBatchLoader.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingDataLoaderExtensions.cs
  12. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs
  13. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs
  14. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs
  15. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs
  16. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  18. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs
  19. 101
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ErrorVisitor.cs
  20. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs
  21. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  23. 18
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  24. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  25. 10
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  26. 51
      backend/src/Squidex.Infrastructure/ResultList.cs
  27. 12
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  28. 5
      backend/src/Squidex/Config/Domain/AssetServices.cs
  29. 3
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  30. 2
      backend/src/Squidex/Config/Domain/StoreServices.cs
  31. 22
      backend/src/Squidex/Squidex.csproj
  32. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs
  33. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  34. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs
  35. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs
  36. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs
  37. 479
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  38. 789
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  39. 65
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs
  40. 101
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  41. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs
  42. 7
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs
  43. 86
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestQuery.cs

9
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs

@ -35,7 +35,14 @@ public sealed record Component(string Type, JsonObject Data, Schema Schema)
return false;
}
if (!o.TryGetValue(Discriminator, out var found) || found.Value is not string s)
return IsValid(o, out discriminator);
}
public static bool IsValid(JsonObject obj, [MaybeNullWhen(false)] out string discriminator)
{
discriminator = null!;
if (!obj.TryGetValue(Discriminator, out var found) || found.Value is not string s)
{
return false;
}

34
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds;
internal sealed class ReferencesCleaner : IFieldVisitor<JsonValue, ReferencesCleaner.Args>
internal sealed class ReferencesCleaner : IFieldPropertiesVisitor<JsonValue, ReferencesCleaner.Args>
{
private static readonly ReferencesCleaner Instance = new ReferencesCleaner();
@ -27,70 +27,70 @@ internal sealed class ReferencesCleaner : IFieldVisitor<JsonValue, ReferencesCle
{
var args = new Args(value, validIds);
return field.Accept(Instance, args);
return field.RawProperties.Accept(Instance, args);
}
public JsonValue Visit(IField<AssetsFieldProperties> field, Args args)
public JsonValue Visit(ArrayFieldProperties properties, Args args)
{
return CleanIds(args);
return args.Value;
}
public JsonValue Visit(IField<ReferencesFieldProperties> field, Args args)
public JsonValue Visit(AssetsFieldProperties properties, Args args)
{
return CleanIds(args);
}
public JsonValue Visit(IField<BooleanFieldProperties> field, Args args)
public JsonValue Visit(ReferencesFieldProperties properties, Args args)
{
return args.Value;
return CleanIds(args);
}
public JsonValue Visit(IField<ComponentFieldProperties> field, Args args)
public JsonValue Visit(BooleanFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<ComponentsFieldProperties> field, Args args)
public JsonValue Visit(ComponentFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<DateTimeFieldProperties> field, Args args)
public JsonValue Visit(ComponentsFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<GeolocationFieldProperties> field, Args args)
public JsonValue Visit(DateTimeFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<JsonFieldProperties> field, Args args)
public JsonValue Visit(GeolocationFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<NumberFieldProperties> field, Args args)
public JsonValue Visit(JsonFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<StringFieldProperties> field, Args args)
public JsonValue Visit(NumberFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<TagsFieldProperties> field, Args args)
public JsonValue Visit(StringFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IField<UIFieldProperties> field, Args args)
public JsonValue Visit(TagsFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(IArrayField field, Args args)
public JsonValue Visit(UIFieldProperties properties, Args args)
{
return args.Value;
}

34
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -18,7 +18,7 @@ using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Core.ValidateContent;
public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonError? Error), JsonValueConverter.Args>
public sealed class JsonValueConverter : IFieldPropertiesVisitor<(object? Result, JsonError? Error), JsonValueConverter.Args>
{
private static readonly JsonValueConverter Instance = new JsonValueConverter();
@ -36,45 +36,45 @@ public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonErro
var args = new Args(value, serializer, components);
return field.Accept(Instance, args);
return field.RawProperties.Accept(Instance, args);
}
public (object? Result, JsonError? Error) Visit(IField<JsonFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(JsonFieldProperties properties, Args args)
{
return (args.Value, null);
}
public (object? Result, JsonError? Error) Visit(IArrayField field, Args args)
public (object? Result, JsonError? Error) Visit(ArrayFieldProperties properties, Args args)
{
return ConvertToObjectList(args.Value);
}
public (object? Result, JsonError? Error) Visit(IField<AssetsFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(AssetsFieldProperties properties, Args args)
{
return ConvertToIdList(args.Value);
}
public (object? Result, JsonError? Error) Visit(IField<ComponentFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(ComponentFieldProperties properties, Args args)
{
return ConvertToComponent(args.Value, args.Components, field.Properties.SchemaIds);
return ConvertToComponent(args.Value, args.Components, properties.SchemaIds);
}
public (object? Result, JsonError? Error) Visit(IField<ComponentsFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(ComponentsFieldProperties properties, Args args)
{
return ConvertToComponentList(args.Value, args.Components, field.Properties.SchemaIds);
return ConvertToComponentList(args.Value, args.Components, properties.SchemaIds);
}
public (object? Result, JsonError? Error) Visit(IField<ReferencesFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(ReferencesFieldProperties properties, Args args)
{
return ConvertToIdList(args.Value);
}
public (object? Result, JsonError? Error) Visit(IField<TagsFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(TagsFieldProperties properties, Args args)
{
return ConvertToStringList(args.Value);
}
public (object? Result, JsonError? Error) Visit(IField<BooleanFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(BooleanFieldProperties properties, Args args)
{
if (args.Value.Value is bool b)
{
@ -84,7 +84,7 @@ public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonErro
return (null, new JsonError(T.Get("contents.invalidBoolean")));
}
public (object? Result, JsonError? Error) Visit(IField<NumberFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(NumberFieldProperties properties, Args args)
{
if (args.Value.Value is double d)
{
@ -94,7 +94,7 @@ public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonErro
return (null, new JsonError(T.Get("contents.invalidNumber")));
}
public (object? Result, JsonError? Error) Visit(IField<StringFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(StringFieldProperties properties, Args args)
{
if (args.Value.Value is string s)
{
@ -104,12 +104,12 @@ public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonErro
return (null, new JsonError(T.Get("contents.invalidString")));
}
public (object? Result, JsonError? Error) Visit(IField<UIFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(UIFieldProperties properties, Args args)
{
return (args.Value, null);
}
public (object? Result, JsonError? Error) Visit(IField<DateTimeFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(DateTimeFieldProperties properties, Args args)
{
if (args.Value.Value is string s)
{
@ -126,7 +126,7 @@ public sealed class JsonValueConverter : IFieldVisitor<(object? Result, JsonErro
return (null, new JsonError(T.Get("contents.invalidString")));
}
public (object? Result, JsonError? Error) Visit(IField<GeolocationFieldProperties> field, Args args)
public (object? Result, JsonError? Error) Visit(GeolocationFieldProperties properties, Args args)
{
var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out var value);

30
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ValidateContent;
public sealed class JsonValueValidator : IFieldVisitor<bool, JsonValueValidator.Args>
public sealed class JsonValueValidator : IFieldPropertiesVisitor<bool, JsonValueValidator.Args>
{
private static readonly JsonValueValidator Instance = new JsonValueValidator();
@ -33,35 +33,35 @@ public sealed class JsonValueValidator : IFieldVisitor<bool, JsonValueValidator.
var args = new Args(value, serializer);
return field.Accept(Instance, args);
return field.RawProperties.Accept(Instance, args);
}
public bool Visit(IArrayField field, Args args)
public bool Visit(ArrayFieldProperties properties, Args args)
{
return IsValidObjectList(args.Value);
}
public bool Visit(IField<AssetsFieldProperties> field, Args args)
public bool Visit(AssetsFieldProperties properties, Args args)
{
return IsValidStringList(args.Value);
}
public bool Visit(IField<BooleanFieldProperties> field, Args args)
public bool Visit(BooleanFieldProperties properties, Args args)
{
return args.Value.Value is bool;
}
public bool Visit(IField<ComponentFieldProperties> field, Args args)
public bool Visit(ComponentFieldProperties properties, Args args)
{
return IsValidComponent(args.Value);
}
public bool Visit(IField<ComponentsFieldProperties> field, Args args)
public bool Visit(ComponentsFieldProperties properties, Args args)
{
return IsValidComponentList(args.Value);
}
public bool Visit(IField<DateTimeFieldProperties> field, Args args)
public bool Visit(DateTimeFieldProperties properties, Args args)
{
if (args.Value.Value is string s)
{
@ -73,39 +73,39 @@ public sealed class JsonValueValidator : IFieldVisitor<bool, JsonValueValidator.
return false;
}
public bool Visit(IField<GeolocationFieldProperties> field, Args args)
public bool Visit(GeolocationFieldProperties properties, Args args)
{
var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out _);
return result == GeoJsonParseResult.Success;
}
public bool Visit(IField<JsonFieldProperties> field, Args args)
public bool Visit(JsonFieldProperties properties, Args args)
{
return true;
}
public bool Visit(IField<NumberFieldProperties> field, Args args)
public bool Visit(NumberFieldProperties properties, Args args)
{
return args.Value.Value is double;
}
public bool Visit(IField<ReferencesFieldProperties> field, Args args)
public bool Visit(ReferencesFieldProperties properties, Args args)
{
return IsValidStringList(args.Value);
}
public bool Visit(IField<StringFieldProperties> field, Args args)
public bool Visit(StringFieldProperties properties, Args args)
{
return args.Value.Value is string;
}
public bool Visit(IField<TagsFieldProperties> field, Args args)
public bool Visit(TagsFieldProperties properties, Args args)
{
return IsValidStringList(args.Value);
}
public bool Visit(IField<UIFieldProperties> field, Args args)
public bool Visit(UIFieldProperties properties, Args args)
{
return true;
}

1
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -9,7 +9,6 @@ using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.DomainObject;
using Squidex.Infrastructure;

16
backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs

@ -241,15 +241,21 @@ public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable
private Task InvalidateItAsync(DomainId id, string name)
{
return appCache.RemoveAsync(
// Do not use cancellation here as we already so far.
return appCache.RemoveAsync(new[]
{
GetCacheKey(id),
GetCacheKey(name));
GetCacheKey(name)
});
}
private Task CacheItAsync(IAppEntity app)
{
return Task.WhenAll(
appCache.AddAsync(GetCacheKey(app.Id), app, CacheDuration),
appCache.AddAsync(GetCacheKey(app.Name), app, CacheDuration));
// Do not use cancellation here as we already so far.
return appCache.AddAsync(new[]
{
new KeyValuePair<string, object?>(GetCacheKey(app.Id), app),
new KeyValuePair<string, object?>(GetCacheKey(app.Name), app),
}, CacheDuration);
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -186,7 +186,7 @@ public sealed class AssetQueryService : IAssetQueryService
if (q.Ids is { Count: > 0 })
{
assets = assets.SortSet(x => x.Id, q.Ids);
assets = assets.Sorted(x => x.Id, q.Ids);
}
return await TransformAsync(context, assets, ct);

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

@ -6,7 +6,6 @@
// ==========================================================================
using System.Text;
using Fluid.Values;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;

1
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs

@ -8,7 +8,6 @@
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using static OpenIddict.Abstractions.OpenIddictConstants;
#pragma warning disable IDE0060 // Remove unused parameter

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingBatchLoader.cs

@ -6,7 +6,6 @@
// ==========================================================================
using GraphQL.DataLoader;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
#pragma warning disable MA0048 // File name must match type name

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Cache/CachingDataLoaderExtensions.cs

@ -7,8 +7,6 @@
using GraphQL.DataLoader;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Translations;
using TagLib.IFD.Tags;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Cache;

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs

@ -54,6 +54,7 @@ public sealed class CachingGraphQLResolver : IConfigureExecution
var context = ((GraphQLExecutionContext)options.UserContext!).Context;
options.Schema = await GetSchemaAsync(context.App);
options.HandleError(serviceProvider);
return await next(options);
}

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs

@ -7,7 +7,6 @@
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs

@ -127,7 +127,6 @@ internal sealed class Builder
newSchema.RegisterType(customType);
}
newSchema.RegisterVisitor(ErrorVisitor.Instance);
newSchema.Initialize();
return newSchema;

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs

@ -70,7 +70,9 @@ internal sealed class ComponentGraphType : ObjectGraphType<JsonObject>
return false;
}
return Component.IsValid(json, out var discriminator) && discriminator == schemaId;
JsonValue current = json;
return Component.IsValid(current, out var discriminator) && discriminator == schemaId;
};
}
}

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs

@ -8,17 +8,12 @@
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using GraphQLParser;
using GraphQLParser.AST;
using NodaTime;
using Squidex.CLI.Commands.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Subscriptions;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives;
using Squidex.Infrastructure;
using Squidex.Shared;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs

@ -7,11 +7,9 @@
using GraphQL.Resolvers;
using GraphQL.Types;
using Namotion.Reflection;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure.Json.Objects;

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs

@ -8,7 +8,6 @@
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;

101
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ErrorVisitor.cs

@ -6,8 +6,6 @@
// ==========================================================================
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using GraphQL.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -16,104 +14,31 @@ using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
internal sealed class ErrorVisitor : BaseSchemaNodeVisitor
internal static class ErrorVisitor
{
public static readonly ErrorVisitor Instance = new ErrorVisitor();
internal sealed class ErrorResolver : IFieldResolver
public static void HandleError(this ExecutionOptions options, IServiceProvider services)
{
private readonly IFieldResolver inner;
public ErrorResolver(IFieldResolver inner)
options.UnhandledExceptionDelegate = context =>
{
this.inner = inner;
}
public async ValueTask<object?> ResolveAsync(IResolveFieldContext context)
{
try
{
return await inner.ResolveAsync(context);
}
catch (ValidationException ex)
{
throw new ExecutionError(ex.Message);
}
catch (DomainException ex)
{
throw new ExecutionError(ex.Message);
}
catch (Exception ex)
{
var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>();
var log = services.GetRequiredService<ILoggerFactory>().CreateLogger("GraphQL");
logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name);
throw;
}
}
}
var fieldName = context.FieldContext?.FieldDefinition?.Name;
internal sealed class ErrorSourceStreamResolver : ISourceStreamResolver
{
private readonly ISourceStreamResolver inner;
public ErrorSourceStreamResolver(ISourceStreamResolver inner)
{
this.inner = inner;
}
public async ValueTask<IObservable<object?>> ResolveAsync(IResolveFieldContext context)
{
try
if (!string.IsNullOrWhiteSpace(fieldName))
{
return await inner.ResolveAsync(context);
log.LogError(context.OriginalException, "Failed to resolve field {field}.", fieldName);
}
catch (ValidationException ex)
else
{
throw new ExecutionError(ex.Message);
log.LogError(context.OriginalException, "Failed to resolve execute query.");
}
catch (DomainException ex)
{
throw new ExecutionError(ex.Message);
}
catch (Exception ex)
{
var logFactory = context.RequestServices!.GetRequiredService<ILoggerFactory>();
logFactory.CreateLogger("GraphQL").LogError(ex, "Failed to resolve field {field}.", context.FieldDefinition.Name);
throw;
}
}
}
private ErrorVisitor()
{
}
public override void VisitObjectFieldDefinition(FieldType field, IObjectGraphType type, ISchema schema)
{
if (type.Name.StartsWith("__", StringComparison.Ordinal))
{
return;
}
if (field.StreamResolver != null)
{
if (field.StreamResolver is ErrorSourceStreamResolver)
{
return;
}
field.StreamResolver = new ErrorSourceStreamResolver(field.StreamResolver);
}
else
{
if (field.Resolver is ErrorResolver)
if (context.OriginalException is ValidationException or DomainException)
{
return;
context.Exception = new ExecutionError(context.OriginalException.Message);
}
field.Resolver = new ErrorResolver(field.Resolver ?? NameFieldResolver.Instance);
}
return Task.CompletedTask;
};
}
}

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs

@ -6,7 +6,6 @@
// ==========================================================================
using GraphQL.Resolvers;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Infrastructure;
using Squidex.Shared.Users;

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs

@ -10,7 +10,6 @@ using GraphQL.Types;
using GraphQL.Utilities;
using GraphQLParser;
using GraphQLParser.AST;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives;
using Squidex.Domain.Apps.Entities.Schemas;

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

@ -132,7 +132,7 @@ public sealed class ContentQueryService : IContentQueryService
if (q.Ids is { Count: > 0 })
{
contents = contents.SortSet(x => x.Id, q.Ids);
contents = contents.Sorted(x => x.Id, q.Ids);
}
return await TransformAsync(context, contents, ct);
@ -166,7 +166,7 @@ public sealed class ContentQueryService : IContentQueryService
if (q.Ids is { Count: > 0 })
{
contents = contents.SortSet(x => x.Id, q.Ids);
contents = contents.Sorted(x => x.Id, q.Ids);
}
return await TransformAsync(context, contents, ct);

18
backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs

@ -42,7 +42,7 @@ public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex
foreach (var schema in schemas.Where(IsValid))
{
await InvalidateItAsync(appId, schema.Id, schema.SchemaDef.Name);
await CacheItAsync(schema);
}
return schemas.Where(IsValid).ToList();
@ -221,15 +221,21 @@ public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex
private Task InvalidateItAsync(DomainId appId, DomainId id, string name)
{
return schemaCache.RemoveAsync(
// Do not use cancellation here as we already so far.
return schemaCache.RemoveAsync(new[]
{
GetCacheKey(appId, id),
GetCacheKey(appId, name));
GetCacheKey(appId, name)
});
}
private Task CacheItAsync(ISchemaEntity schema)
{
return Task.WhenAll(
schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration),
schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration));
// Do not use cancellation here as we already so far.
return schemaCache.AddAsync(new[]
{
new KeyValuePair<string, object?>(GetCacheKey(schema.AppId.Id, schema.Id), schema),
new KeyValuePair<string, object?>(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema),
}, CacheDuration);
}
}

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

@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Notifo.SDK" Version="1.7.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.CLI.Core" Version="11.0.0" />
<PackageReference Include="Squidex.CLI.Core" Version="11.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

10
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -147,16 +147,6 @@ public static class CollectionExtensions
return reverse ? source.Reverse() : source;
}
public static IResultList<T> SortSet<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class
{
return ResultList.Create(input.Total, SortList(input, idProvider, ids));
}
public static IEnumerable<T> SortList<T, TKey>(this IEnumerable<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class
{
return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).NotNull();
}
public static IEnumerable<T> Duplicates<T>(this IEnumerable<T> input)
{
return input.GroupBy(x => x).Where(x => x.Count() > 1).Select(x => x.Key);

51
backend/src/Squidex.Infrastructure/ResultList.cs

@ -48,4 +48,55 @@ public static class ResultList
{
return new Impl<T>(items.ToList(), total);
}
public static IResultList<T> Sorted<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where TKey : notnull where T : class
{
if (input.Count == 0)
{
return Empty<T>();
}
var result = new List<T>(ids.Count);
if (input.Count >= 5)
{
var dictionary = new Dictionary<TKey, T>(input.Count);
foreach (var item in input)
{
dictionary[idProvider(item)] = item;
}
foreach (var id in ids)
{
if (dictionary.TryGetValue(id, out var item))
{
result.Add(item);
}
}
}
else
{
foreach (var id in ids)
{
T? item = null;
foreach (var candidate in input)
{
if (Equals(id, idProvider(candidate)))
{
item = candidate;
break;
}
}
if (item != null)
{
result.Add(item);
}
}
}
return Create(input.Total, result);
}
}

12
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -24,12 +24,12 @@
<PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Include="OpenTelemetry.Api" Version="1.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="5.13.0" />
<PackageReference Include="Squidex.Caching" Version="5.13.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="5.14.0" />
<PackageReference Include="Squidex.Log" Version="5.13.0" />
<PackageReference Include="Squidex.Messaging" Version="5.14.0" />
<PackageReference Include="Squidex.Text" Version="5.13.0" />
<PackageReference Include="Squidex.Assets" Version="5.16.0" />
<PackageReference Include="Squidex.Caching" Version="5.16.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="5.16.0" />
<PackageReference Include="Squidex.Log" Version="5.16.0" />
<PackageReference Include="Squidex.Messaging" Version="5.16.0" />
<PackageReference Include="Squidex.Text" Version="5.16.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

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

@ -5,19 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using FluentFTP;
using MongoDB.Driver.GridFS;
using Squidex.Assets;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Queries;
using Squidex.Domain.Apps.Entities.Assets.Queries.Steps;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Search;
using Squidex.Hosting;
using Squidex.Infrastructure.EventSourcing;
using tusdotnet.FileLocks;
using tusdotnet.Interfaces;
namespace Squidex.Config.Domain;

3
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.News;
@ -27,8 +26,6 @@ using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline.Robots;
using Squidex.Shared;
using Squidex.Text.Translations;
using Squidex.Text.Translations.GoogleCloud;
using Squidex.Web;
using Squidex.Web.Pipeline;

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

@ -13,7 +13,6 @@ using Migrations.Migrations.MongoDb;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Core.Extensions.DiagnosticSources;
using Squidex.Assets;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
@ -42,7 +41,6 @@ using Squidex.Domain.Apps.Entities.Teams.Repositories;
using Squidex.Domain.Users;
using Squidex.Domain.Users.InMemory;
using Squidex.Domain.Users.MongoDb;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Diagnostics;

22
backend/src/Squidex/Squidex.csproj

@ -62,18 +62,18 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc7" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.1.22" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="5.14.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="5.14.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="5.14.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="5.13.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="5.13.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="5.14.0" />
<PackageReference Include="Squidex.Assets.S3" Version="5.13.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="5.14.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="5.16.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="5.16.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="5.16.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="5.16.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="5.16.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="5.16.0" />
<PackageReference Include="Squidex.Assets.S3" Version="5.16.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="5.16.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="16.0.0" />
<PackageReference Include="Squidex.Hosting" Version="5.13.0" />
<PackageReference Include="Squidex.Messaging.All" Version="5.14.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.13.0" />
<PackageReference Include="Squidex.Hosting" Version="5.16.0" />
<PackageReference Include="Squidex.Messaging.All" Version="5.16.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.16.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>

1
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;

1
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -7,7 +7,6 @@
using System.Net;
using System.Text;
using Jint.Runtime;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Scripting;

1
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Logging;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;

1
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Logging;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;

11
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
@ -22,7 +21,9 @@ public class GraphQLIntrospectionTests : GraphQLTestBase
[Fact]
public async Task Should_introspect()
{
const string query = @"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
query IntrospectionQuery {
__schema {
queryType {
@ -107,9 +108,9 @@ public class GraphQLIntrospectionTests : GraphQLTestBase
}
}
}
}";
var actual = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" });
}",
OperationName = "IntrospectionQuery"
});
var json = serializer.Serialize(actual);

479
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs

@ -5,11 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using GraphQL;
using NodaTime.Text;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -34,14 +31,15 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permission_to_create()
{
var query = @"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
createMySchemaContent(data: { myNumber: { iv: 42 } }) {
createMySchemaContent(data: { }) {
id
}
}";
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}"
});
var expected = new
{
@ -79,18 +77,26 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_single_content_if_creating_content()
{
var query = CreateQuery(@"
mutation {
createMySchemaContent(data: <DATA>, publish: true) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsCreate;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation MyMutation($data: MySchemaDataInputDto!) {
createMySchemaContent(data: $data, publish: true) {
{fields}
}
}",
Args = new
{
fields = TestContent.AllFields
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id),
},
Permission = PermissionIds.AppContentsCreate
});
var expected = new
{
@ -115,55 +121,27 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_single_content_if_creating_content_with_custom_id()
{
var query = CreateQuery(@"
mutation {
createMySchemaContent(data: <DATA>, id: '123', publish: true) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsCreate;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var expected = new
var actual = await ExecuteAsync(new TestQuery
{
data = new
{
createMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, actual);
A.CallTo(() => commandBus.PublishAsync(
A<CreateContent>.That.Matches(x =>
x.ExpectedVersion == EtagVersion.Any &&
x.ContentId == DomainId.Create("123") &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Status == Status.Published &&
x.Data.Equals(content.Data)),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_if_creating_content_with_variable()
{
var query = CreateQuery(@"
mutation OP($data: MySchemaDataInputDto!) {
createMySchemaContent(data: $data, publish: true) {
<FIELDS_CONTENT>
Query = @"
mutation MyMutation($data: MySchemaDataInputDto!) {
createMySchemaContent(data: $data, id: '{contentId}', publish: true) {
{fields}
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsCreate;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission);
}",
Args = new
{
contentId,
fields = TestContent.AllFields
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id)
},
Permission = PermissionIds.AppContentsCreate
});
var expected = new
{
@ -178,6 +156,7 @@ public class GraphQLMutationTests : GraphQLTestBase
A.CallTo(() => commandBus.PublishAsync(
A<CreateContent>.That.Matches(x =>
x.ExpectedVersion == EtagVersion.Any &&
x.ContentId == contentId &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Status == Status.Published &&
x.Data.Equals(content.Data)),
@ -188,14 +167,19 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permission_to_update()
{
var query = CreateQuery(@"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
updateMySchemaContent(id: '<ID>', data: { myNumber: { iv: 42 } }) {
updateMySchemaContent(id: '{contentId}', data: { }) {
id
}
}", contentId, content);
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}",
Args = new
{
contentId
}
});
var expected = new
{
@ -233,54 +217,27 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_single_content_if_updating_content()
{
var query = CreateQuery(@"
mutation {
updateMySchemaContent(id: '<ID>', data: <DATA>, expectedVersion: 10) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsUpdateOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var expected = new
var actual = await ExecuteAsync(new TestQuery
{
data = new
{
updateMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, actual);
A.CallTo(() => commandBus.PublishAsync(
A<UpdateContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ExpectedVersion == 10 &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Data.Equals(content.Data)),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_if_updating_content_with_variable()
{
var query = CreateQuery(@"
mutation OP($data: MySchemaDataInputDto!) {
updateMySchemaContent(id: '<ID>', data: $data, expectedVersion: 10) {
<FIELDS_CONTENT>
Query = @"
mutation MyMutation($data: MySchemaDataInputDto!) {
updateMySchemaContent(id: '{contentId}', data: $data, expectedVersion: 10) {
{fields}
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsUpdateOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission);
}",
Args = new
{
contentId,
fields = TestContent.AllFields
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id)
},
Permission = PermissionIds.AppContentsUpdateOwn
});
var expected = new
{
@ -294,7 +251,7 @@ public class GraphQLMutationTests : GraphQLTestBase
A.CallTo(() => commandBus.PublishAsync(
A<UpdateContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ContentId == contentId &&
x.ExpectedVersion == 10 &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Data.Equals(content.Data)),
@ -305,14 +262,15 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permission_to_upsert()
{
var query = CreateQuery(@"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
upsertMySchemaContent(id: '<ID>', data: { myNumber: { iv: 42 } }) {
upsertMySchemaContent(id: '{contentId}', data: { }) {
id
}
}", contentId, content);
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}"
});
var expected = new
{
@ -350,55 +308,27 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_single_content_if_upserting_content()
{
var query = CreateQuery(@"
mutation {
upsertMySchemaContent(id: '<ID>', data: <DATA>, publish: true, expectedVersion: 10) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsUpsert;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var expected = new
var actual = await ExecuteAsync(new TestQuery
{
data = new
{
upsertMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, actual);
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ExpectedVersion == 10 &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Status == Status.Published &&
x.Data.Equals(content.Data)),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_if_upserting_content_with_variable()
{
var query = CreateQuery(@"
mutation OP($data: MySchemaDataInputDto!) {
upsertMySchemaContent(id: '<ID>', data: $data, publish: true, expectedVersion: 10) {
<FIELDS_CONTENT>
Query = @"
mutation MyMutation($data: MySchemaDataInputDto!) {
upsertMySchemaContent(id: '{contentId}', data: $data, publish: true, expectedVersion: 10) {
{fields}
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsUpsert;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission);
}",
Args = new
{
contentId,
fields = TestContent.AllFields
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id)
},
Permission = PermissionIds.AppContentsUpsert
});
var expected = new
{
@ -412,7 +342,7 @@ public class GraphQLMutationTests : GraphQLTestBase
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ContentId == contentId &&
x.ExpectedVersion == 10 &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Status == Status.Published &&
@ -424,14 +354,19 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permission_to_patch()
{
var query = CreateQuery(@"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
patchMySchemaContent(id: '<ID>', data: { myNumber: { iv: 42 } }) {
patchMySchemaContent(id: '{contentId}', data: { }) {
id
}
}", contentId, content);
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}",
Args = new
{
contentId
},
});
var expected = new
{
@ -469,54 +404,27 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_single_content_if_patching_content()
{
var query = CreateQuery(@"
mutation {
patchMySchemaContent(id: '<ID>', data: <DATA>, expectedVersion: 10) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsUpdateOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var expected = new
var actual = await ExecuteAsync(new TestQuery
{
data = new
{
patchMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, actual);
A.CallTo(() => commandBus.PublishAsync(
A<PatchContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ExpectedVersion == 10 &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Data.Equals(content.Data)),
A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_if_patching_content_with_variable()
{
var query = CreateQuery(@"
mutation OP($data: MySchemaDataInputDto!) {
patchMySchemaContent(id: '<ID>', data: $data, expectedVersion: 10) {
<FIELDS_CONTENT>
Query = @"
mutation MyMutation($data: MySchemaDataInputDto!) {
patchMySchemaContent(id: '{contentId}', data: $data, expectedVersion: 10) {
{fields}
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsUpdateOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, permission);
}",
Args = new
{
contentId,
fields = TestContent.AllFields
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id)
},
Permission = PermissionIds.AppContentsUpdateOwn
});
var expected = new
{
@ -530,7 +438,7 @@ public class GraphQLMutationTests : GraphQLTestBase
A.CallTo(() => commandBus.PublishAsync(
A<PatchContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ContentId == contentId &&
x.ExpectedVersion == 10 &&
x.SchemaId.Equals(TestSchemas.DefaultId) &&
x.Data.Equals(content.Data)),
@ -541,14 +449,19 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permission_to_change_status()
{
var query = CreateQuery(@"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
changeMySchemaContent(id: '<ID>', status: 'Published') {
changeMySchemaContent(id: '{contentId}', status: 'Published') {
id
}
}", contentId, content);
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}",
Args = new
{
contentId
},
});
var expected = new
{
@ -588,18 +501,23 @@ public class GraphQLMutationTests : GraphQLTestBase
{
var dueTime = InstantPattern.General.Parse("2021-12-12T11:10:09Z").Value;
var query = CreateQuery(@"
mutation {
changeMySchemaContent(id: '<ID>', status: 'Published', dueTime: '2021-12-12T11:10:09Z', expectedVersion: 10) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsChangeStatusOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
changeMySchemaContent(id: '{contentId}', status: 'Published', dueTime: '2021-12-12T11:10:09Z', expectedVersion: 10) {
{fields}
}
}",
Args = new
{
contentId,
fields = TestContent.AllFields
},
Permission = PermissionIds.AppContentsChangeStatusOwn
});
var expected = new
{
@ -625,18 +543,23 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_single_content_if_changing_status_without_due_time()
{
var query = CreateQuery(@"
mutation {
changeMySchemaContent(id: '<ID>', status: 'Published', expectedVersion: 10) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsChangeStatusOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
changeMySchemaContent(id: '{contentId}', status: 'Published', expectedVersion: 10) {
{fields}
}
}",
Args = new
{
contentId,
fields = TestContent.AllFields
},
Permission = PermissionIds.AppContentsChangeStatusOwn
});
var expected = new
{
@ -662,18 +585,23 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_single_content_if_changing_status_with_null_due_time()
{
var query = CreateQuery(@"
mutation {
changeMySchemaContent(id: '<ID>', status: 'Published', dueTime: null, expectedVersion: 10) {
<FIELDS_CONTENT>
}
}", contentId, content);
commandContext.Complete(content);
var permission = PermissionIds.AppContentsChangeStatusOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
changeMySchemaContent(id: '{contentId}', status: 'Published', dueTime: null, expectedVersion: 10) {
{fields}
}
}",
Args = new
{
contentId,
fields = TestContent.AllFields
},
Permission = PermissionIds.AppContentsChangeStatusOwn
});
var expected = new
{
@ -699,14 +627,19 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permission_to_delete()
{
var query = CreateQuery(@"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
deleteMySchemaContent(id: '<ID>') {
deleteMySchemaContent(id: '{contentId}') {
version
}
}", contentId, content);
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}",
Args = new
{
contentId
},
});
var expected = new
{
@ -741,18 +674,22 @@ public class GraphQLMutationTests : GraphQLTestBase
[Fact]
public async Task Should_return_new_version_if_deleting_content()
{
var query = CreateQuery(@"
commandContext.Complete(CommandResult.Empty(contentId, 1, 0));
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
mutation {
deleteMySchemaContent(id: '<ID>', expectedVersion: 10) {
version
deleteMySchemaContent(id: '{contentId}', expectedVersion: 10) {
version
}
}", contentId, content);
commandContext.Complete(CommandResult.Empty(contentId, 13, 12));
var permission = PermissionIds.AppContentsDeleteOwn;
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission);
}",
Args = new
{
contentId
},
Permission = PermissionIds.AppContentsDeleteOwn
});
var expected = new
{
@ -760,7 +697,7 @@ public class GraphQLMutationTests : GraphQLTestBase
{
deleteMySchemaContent = new
{
version = 13
version = 1
}
}
};
@ -775,16 +712,4 @@ public class GraphQLMutationTests : GraphQLTestBase
A<CancellationToken>._))
.MustHaveHappened();
}
private Inputs GetInput()
{
var input = new
{
data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id)
};
var element = JsonSerializer.SerializeToElement(input, TestUtils.DefaultOptions());
return serializer.ReadNode<Inputs>(element)!;
}
}

789
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

File diff suppressed because it is too large

65
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLSubscriptionTests.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System.Reactive.Linq;
using GraphQL;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Subscriptions;
@ -22,15 +21,6 @@ public class GraphQLSubscriptionTests : GraphQLTestBase
{
var id = DomainId.NewGuid();
var query = CreateQuery(@"
subscription {
assetChanges {
id,
fileName,
fileSize
}
}");
var stream =
Observable.Return<object>(
new EnrichedAssetEvent
@ -43,9 +33,18 @@ public class GraphQLSubscriptionTests : GraphQLTestBase
A.CallTo(() => subscriptionService.Subscribe<object>(A<AssetSubscription>._))
.Returns(stream);
var permission = PermissionIds.ForApp(PermissionIds.AppAssetsRead, TestApp.Default.Name);
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id);
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
subscription {
assetChanges {
id,
fileName,
fileSize
}
}",
Permission = PermissionIds.AppAssetsRead
});
var expected = new
{
@ -66,16 +65,17 @@ public class GraphQLSubscriptionTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permissions_for_assets()
{
var query = CreateQuery(@"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
subscription {
assetChanges {
id,
fileName,
fileSize
}
}");
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}"
});
var expected = new
{
@ -109,14 +109,6 @@ public class GraphQLSubscriptionTests : GraphQLTestBase
{
var id = DomainId.NewGuid();
var query = CreateQuery(@"
subscription {
contentChanges {
id,
data
}
}");
var stream =
Observable.Return<object>(
new EnrichedContentEvent
@ -131,9 +123,17 @@ public class GraphQLSubscriptionTests : GraphQLTestBase
A.CallTo(() => subscriptionService.Subscribe<object>(A<ContentSubscription>._))
.Returns(stream);
var permission = PermissionIds.ForApp(PermissionIds.AppContentsRead, TestApp.Default.Name, "random-schema");
var actual = await ExecuteAsync(new ExecutionOptions { Query = query }, permission.Id);
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
subscription {
contentChanges {
id,
data
}
}",
Permission = PermissionIds.AppContentsRead
});
var expected = new
{
@ -159,15 +159,16 @@ public class GraphQLSubscriptionTests : GraphQLTestBase
[Fact]
public async Task Should_return_error_if_user_has_no_permissions_for_contents()
{
var query = CreateQuery(@"
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
subscription {
contentChanges {
id,
data
}
}");
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
}"
});
var expected = new
{

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

@ -7,7 +7,6 @@
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Text.RegularExpressions;
using GraphQL;
using GraphQL.DataLoader;
using GraphQL.Execution;
@ -25,7 +24,6 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Messaging.Subscriptions;
using Squidex.Shared;
using Squidex.Shared.Users;
#pragma warning disable SA1401 // Fields must be private
@ -43,6 +41,11 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
protected readonly Context requestContext;
private CachingGraphQLResolver? sut;
protected class QueryOptions
{
public string Query { get; set; }
}
protected GraphQLTestBase()
{
A.CallTo(() => userResolver.QueryManyAsync(A<string[]>._, default))
@ -50,7 +53,11 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
{
var ids = x.GetArgument<string[]>(0)!;
var users = ids.Select(id => UserMocks.User(id, $"{id}@email.com", $"name_{id}"));
var users = ids.Select(id =>
UserMocks.User(
id,
$"{id}@email.com",
$"{id}name"));
return Task.FromResult(users.ToDictionary(x => x.Id));
});
@ -66,34 +73,19 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
Assert.Equal(isonOutputExpected, jsonOutputResult);
}
protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options)
{
return ExecuteCoreAsync(options, requestContext);
}
protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, string permissionId)
{
return ExecuteCoreAsync(options, BuildContext(permissionId));
}
protected async Task<ExecutionResult> ExecuteCoreAsync(ExecutionOptions options, Context context)
protected Task<ExecutionResult> ExecuteAsync(TestQuery query)
{
// Use a shared instance to test caching.
sut ??= CreateSut(TestSchemas.Default, TestSchemas.Ref1, TestSchemas.Ref2);
// Provide the context to the test if services need to be resolved.
var graphQLContext = ActivatorUtilities.CreateInstance<GraphQLExecutionContext>(sut.Services, context)!;
options.UserContext = graphQLContext;
var options = query.ToOptions(sut.Services);
// Register data loader and other listeners.
foreach (var listener in sut.Services.GetRequiredService<IEnumerable<IDocumentExecutionListener>>())
{
options.Listeners.Add(listener);
}
return ExecuteAsync(sut, options);
}
// Enrich the context with the schema.
await sut.ExecuteAsync(options, x => Task.FromResult<ExecutionResult>(null!));
private static async Task<ExecutionResult> ExecuteAsync(CachingGraphQLResolver resolver, ExecutionOptions options)
{
await resolver.ExecuteAsync(options, x => Task.FromResult<ExecutionResult>(null!));
var actual = await new DocumentExecuter().ExecuteAsync(options);
@ -111,13 +103,6 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
return actual;
}
private static Context BuildContext(string permissionId)
{
var permission = PermissionIds.ForApp(permissionId, TestApp.Default.Name, TestSchemas.DefaultId.Name).Id;
return new Context(Mocks.FrontendUser(permission: permission), TestApp.Default);
}
protected CachingGraphQLResolver CreateSut(params ISchemaEntity[] schemas)
{
var appProvider = A.Fake<IAppProvider>();
@ -127,9 +112,10 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
var serviceProvider =
new ServiceCollection()
.AddLogging()
.AddMemoryCache()
.AddBackgroundCache()
.AddLogging(options =>
{
options.AddDebug();
})
.Configure<AssetOptions>(x =>
{
x.CanCache = true;
@ -153,6 +139,8 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
A.Fake<ILoggerFactory>())
.AddSingleton(
A.Fake<ISchemasHash>())
.AddMemoryCache()
.AddBackgroundCache()
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
.AddSingleton(commandBus)
@ -164,54 +152,19 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
return ActivatorUtilities.CreateInstance<CachingGraphQLResolver>(serviceProvider);
}
protected static string CreateQuery(string query, DomainId id = default, IEnrichedContentEntity? content = null)
{
query = query
.Replace('\'', '"')
.Replace('`', '"')
.Replace("<FIELDS_ASSET>", TestAsset.AllFields, StringComparison.Ordinal)
.Replace("<FIELDS_CONTENT>", TestContent.AllFields, StringComparison.Ordinal)
.Replace("<FIELDS_CONTENT_FLAT>", TestContent.AllFlatFields, StringComparison.Ordinal);
if (id != default)
{
query = query.Replace("<ID>", id.ToString(), StringComparison.Ordinal);
}
if (query.Contains("<DATA>", StringComparison.Ordinal) && content != null)
{
var data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id);
// Json is not the same as the input format of graphql, therefore we need to convert it.
var dataJson = TestUtils.DefaultSerializer.Serialize(data, true);
// Use properties without quotes.
dataJson = Regex.Replace(dataJson, "\"([^\"]+)\":", x => $"{x.Groups[1].Value}:");
// Use enum values whithout quotes.
dataJson = Regex.Replace(dataJson, "\"Enum([A-Za-z]+)\"", x => $"Enum{x.Groups[1].Value}");
query = query.Replace("<DATA>", dataJson, StringComparison.Ordinal);
}
return query;
}
protected Context MatchsAssetContext()
protected static Context MatchsAssetContext()
{
return A<Context>.That.Matches(x =>
x.App == TestApp.Default &&
x.NoCleanup() &&
x.NoAssetEnrichment() &&
x.UserPrincipal == requestContext.UserPrincipal);
x.NoAssetEnrichment());
}
protected Context MatchsContentContext()
protected static Context MatchsContentContext()
{
return A<Context>.That.Matches(x =>
x.App == TestApp.Default &&
x.NoCleanup() &&
x.NoEnrichment() &&
x.UserPrincipal == requestContext.UserPrincipal);
x.NoEnrichment());
}
}

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

@ -100,7 +100,7 @@ public static class TestAsset
{
id = asset.CreatedBy.Identifier,
email = $"{asset.CreatedBy.Identifier}@email.com",
displayName = $"name_{asset.CreatedBy.Identifier}"
displayName = $"{asset.CreatedBy.Identifier}name"
},
editToken = $"token_{asset.Id}",
lastModified = asset.LastModified,

7
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs

@ -7,7 +7,6 @@
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -149,7 +148,7 @@ public static class TestContent
url
flatData {
myJson
myJsonValue: myJson(path: ""value"")
myJsonValue: myJson(path: 'value')
myJson2 {
__typename
rootString,
@ -375,7 +374,7 @@ public static class TestContent
{
id = content.CreatedBy.Identifier,
email = $"{content.CreatedBy.Identifier}@email.com",
displayName = $"name_{content.CreatedBy.Identifier}"
displayName = $"{content.CreatedBy.Identifier}name"
},
editToken = $"token_{content.Id}",
lastModified = content.LastModified,
@ -407,7 +406,7 @@ public static class TestContent
{
id = content.CreatedBy.Identifier,
email = $"{content.CreatedBy.Identifier}@email.com",
displayName = $"name_{content.CreatedBy.Identifier}"
displayName = $"{content.CreatedBy.Identifier}name"
},
editToken = $"token_{content.Id}",
lastModified = content.LastModified,

86
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestQuery.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using GraphQL;
using GraphQL.Execution;
using GraphQL.SystemTextJson;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL;
public sealed class TestQuery
{
private static readonly GraphQLSerializer Serializer = new GraphQLSerializer(TestUtils.DefaultOptions());
required public string Query { get; set; }
public object? Variables { get; set; }
public object? Args { get; set; }
public string? Permission { get; set; }
public string? OperationName { get; set; }
public ExecutionOptions ToOptions(IServiceProvider services)
{
var query = Query;
if (Args != null)
{
foreach (var property in Serialize(Args).EnumerateObject())
{
query = query.Replace($"{{{property.Name}}}", property.Value.ToString(), StringComparison.Ordinal);
}
}
var options = new ExecutionOptions
{
Query = query.Replace('\'', '\"')
};
if (OperationName != null)
{
options.OperationName = OperationName;
}
if (Variables != null)
{
options.Variables = Serializer.ReadNode<Inputs>(Serialize(Variables))!;
}
foreach (var listener in services.GetRequiredService<IEnumerable<IDocumentExecutionListener>>())
{
options.Listeners.Add(listener);
}
options.UserContext = ActivatorUtilities.CreateInstance<GraphQLExecutionContext>(services, BuildContext(Permission));
return options;
static Context BuildContext(string? permissionId)
{
if (permissionId == null)
{
return new Context(Mocks.FrontendUser(), TestApp.Default);
}
var permission = PermissionIds.ForApp(permissionId, TestApp.Default.Name, TestSchemas.DefaultId.Name).Id;
return new Context(Mocks.FrontendUser(permission: permission), TestApp.Default);
}
static JsonElement Serialize(object value)
{
return JsonSerializer.SerializeToElement(value, TestUtils.DefaultOptions());
}
}
}
Loading…
Cancel
Save