Browse Source

Update graphql. (#880)

* Update graphql.

* Fix reading.
pull/881/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
2cfa0c7e47
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs
  2. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  4. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs
  6. 30
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs
  7. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs
  8. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs
  9. 72
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonNoopGraphType.cs
  11. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs
  12. 26
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
  13. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs
  14. 4
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  15. 35
      backend/src/Squidex.Web/GraphQL/BufferingDocumentWriter.cs
  16. 61
      backend/src/Squidex.Web/GraphQL/BufferingGraphQLSerializer.cs
  17. 3
      backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs
  18. 7
      backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs
  19. 2
      backend/src/Squidex.Web/Services/UrlGenerator.cs
  20. 5
      backend/src/Squidex.Web/Squidex.Web.csproj
  21. 24
      backend/src/Squidex/Config/Domain/SerializationServices.cs
  22. 36
      backend/src/Squidex/Config/Web/WebServices.cs
  23. 7
      backend/src/Squidex/Squidex.csproj
  24. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs
  25. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  26. 22
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  27. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

2
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
public sealed class StringReferenceExtractor public sealed class StringReferenceExtractor
{ {
private readonly List<Regex> contentsPatterns = new List<Regex>(); private readonly List<Regex> contentsPatterns = new List<Regex>();
private readonly List<Regex> assetsPatterns = new List<Regex>(); private readonly List<Regex> assetsPatterns = new List<Regex>();
public StringReferenceExtractor(IUrlGenerator urlGenerator) public StringReferenceExtractor(IUrlGenerator urlGenerator)

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

@ -47,11 +47,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.options = options.Value; this.options = options.Value;
} }
public async Task ConfigureAsync(ExecutionOptions executionOptions) public async Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, ExecutionDelegate next)
{ {
var context = ((GraphQLExecutionContext)executionOptions.UserContext!).Context; var context = ((GraphQLExecutionContext)options.UserContext!).Context;
executionOptions.Schema = await GetSchemaAsync(context.App); options.Schema = await GetSchemaAsync(context.App);
return await next(options);
} }
public async Task<GraphQLSchema> GetSchemaAsync(IAppEntity app) public async Task<GraphQLSchema> GetSchemaAsync(IAppEntity app)

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -151,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private IDataLoader<DomainId, IEnrichedAssetEntity> GetAssetsLoader() private IDataLoader<DomainId, IEnrichedAssetEntity> GetAssetsLoader()
{ {
return dataLoaders.Context.GetOrAddBatchLoader<DomainId, IEnrichedAssetEntity>(nameof(GetAssetsLoader), return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedAssetEntity>(nameof(GetAssetsLoader),
async (batch, ct) => async (batch, ct) =>
{ {
var result = await GetReferencedAssetsAsync(new List<DomainId>(batch), ct); var result = await GetReferencedAssetsAsync(new List<DomainId>(batch), ct);
@ -162,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private IDataLoader<DomainId, IEnrichedContentEntity> GetContentsLoader() private IDataLoader<DomainId, IEnrichedContentEntity> GetContentsLoader()
{ {
return dataLoaders.Context.GetOrAddBatchLoader<DomainId, IEnrichedContentEntity>(nameof(GetContentsLoader), return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedContentEntity>(nameof(GetContentsLoader),
async (batch, ct) => async (batch, ct) =>
{ {
var result = await GetReferencedContentsAsync(new List<DomainId>(batch), ct); var result = await GetReferencedContentsAsync(new List<DomainId>(batch), ct);
@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private IDataLoader<string, IUser> GetUserLoader() private IDataLoader<string, IUser> GetUserLoader()
{ {
return dataLoaders.Context.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader), return dataLoaders.Context!.GetOrAddBatchLoader<string, IUser>(nameof(GetUserLoader),
async (batch, ct) => async (batch, ct) =>
{ {
var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct); var result = await Resolve<IUserResolver>().QueryManyAsync(batch.ToArray(), ct);

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

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
} }
}; };
public static readonly ValueResolver Resolver = (value, fieldContext, context) => public static readonly ValueResolver<object> Resolver = (value, fieldContext, context) =>
{ {
if (fieldContext.Arguments != null && if (fieldContext.Arguments != null &&
fieldContext.Arguments.TryGetValue("path", out var contextValue) && fieldContext.Arguments.TryGetValue("path", out var contextValue) &&

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

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
foreach (var value in values) foreach (var value in values)
{ {
AddValue(value, null, value); Add(value, value, value);
} }
} }

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

@ -17,7 +17,9 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{ {
public delegate object ValueResolver(IJsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context); public delegate T ValueResolver<T>(IJsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context);
public delegate Task<T> AsyncValueResolver<T>(IJsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context);
internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo> internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo>
{ {
@ -79,14 +81,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
} }
}); });
private static readonly IFieldResolver Assets = CreateValueResolver((value, fieldContext, context) => private static readonly IFieldResolver Assets = CreateAsyncValueResolver((value, fieldContext, context) =>
{ {
var cacheDuration = fieldContext.CacheDuration(); var cacheDuration = fieldContext.CacheDuration();
return context.GetReferencedAssetsAsync(value, cacheDuration, fieldContext.CancellationToken); return context.GetReferencedAssetsAsync(value, cacheDuration, fieldContext.CancellationToken);
}); });
private static readonly IFieldResolver References = CreateValueResolver((value, fieldContext, context) => private static readonly IFieldResolver References = CreateAsyncValueResolver((value, fieldContext, context) =>
{ {
var cacheDuration = fieldContext.CacheDuration(); var cacheDuration = fieldContext.CacheDuration();
@ -274,7 +276,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
return componentType; return componentType;
} }
private static IFieldResolver CreateValueResolver(ValueResolver valueResolver) private static IFieldResolver CreateValueResolver<T>(ValueResolver<T> valueResolver)
{ {
return Resolvers.Sync<IReadOnlyDictionary<string, IJsonValue>, object?>((source, fieldContext, context) => return Resolvers.Sync<IReadOnlyDictionary<string, IJsonValue>, object?>((source, fieldContext, context) =>
{ {
@ -293,5 +295,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
return null; return null;
}); });
} }
private static IFieldResolver CreateAsyncValueResolver<T>(AsyncValueResolver<T> valueResolver)
{
return Resolvers.Async<IReadOnlyDictionary<string, IJsonValue>, object?>(async (source, fieldContext, context) =>
{
var key = fieldContext.FieldDefinition.SourceName();
if (source.TryGetValue(key, out var value))
{
if (value is JsonNull)
{
return null;
}
return await valueResolver(value, fieldContext, context);
}
return null;
});
}
} }
} }

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Directives/CacheDirective.cs

@ -6,10 +6,11 @@
// ========================================================================== // ==========================================================================
using GraphQL.Types; using GraphQL.Types;
using GraphQLParser.AST;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives
{ {
public sealed class CacheDirective : DirectiveGraphType public sealed class CacheDirective : Directive
{ {
public CacheDirective() public CacheDirective()
: base("cache", DirectiveLocation.Field, DirectiveLocation.FragmentSpread, DirectiveLocation.InlineFragment) : base("cache", DirectiveLocation.Field, DirectiveLocation.FragmentSpread, DirectiveLocation.InlineFragment)

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs

@ -5,8 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GraphQL.Language.AST;
using GraphQL.Types; using GraphQL.Types;
using GraphQLParser.AST;
using NodaTime.Text; using NodaTime.Text;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
@ -23,11 +23,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
return InstantPattern.ExtendedIso.Parse(value?.ToString()!).Value; return InstantPattern.ExtendedIso.Parse(value?.ToString()!).Value;
} }
public override object? ParseLiteral(IValue value) public override object? ParseLiteral(GraphQLValue value)
{ {
switch (value) switch (value)
{ {
case StringValue stringValue: case GraphQLStringValue stringValue:
return ParseValue(stringValue.Value); return ParseValue(stringValue.Value);
default: default:
return null; return null;

72
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs

@ -5,7 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GraphQL.Language.AST; using System.Globalization;
using GraphQLParser.AST;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
@ -22,23 +23,50 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
return ParseJson(value); return ParseJson(value);
} }
public static IJsonValue ParseJson(object? value) public static IJsonValue ParseJson(object? input)
{ {
switch (value) switch (input)
{ {
case ListValue listValue: case GraphQLBooleanValue booleanValue:
return ParseJson(listValue.Value); return JsonValue.Create(booleanValue.BoolValue);
case ObjectValue objectValue: case GraphQLFloatValue floatValue:
return ParseJson(objectValue.Value); return JsonValue.Create(double.Parse((string)floatValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture));
case IReadOnlyDictionary<string, object> dictionary: case GraphQLIntValue intValue:
return JsonValue.Create(int.Parse((string)intValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture));
case GraphQLNullValue:
return JsonValue.Null;
case GraphQLStringValue stringValue:
return JsonValue.Create((string)stringValue.Value);
case GraphQLListValue listValue:
{
var json = JsonValue.Array();
if (listValue.Values != null)
{
foreach (var item in listValue.Values)
{
json.Add(ParseJson(item));
}
}
return json;
}
case GraphQLObjectValue objectValue:
{ {
var json = JsonValue.Object(); var json = JsonValue.Object();
foreach (var (key, inner) in dictionary) if (objectValue.Fields != null)
{ {
json[key] = ParseJson(inner); foreach (var field in objectValue.Fields)
{
json[field.Name.ToString()] = ParseJson(field.Value);
}
} }
return json; return json;
@ -46,22 +74,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
case IEnumerable<object> list: case IEnumerable<object> list:
{ {
var array = JsonValue.Array(); var json = JsonValue.Array();
foreach (var item in list) foreach (var item in list)
{ {
array.Add(ParseJson(item)); json.Add(ParseJson(item));
} }
return array; return json;
}
case IDictionary<string, object> obj:
{
var json = JsonValue.Object();
foreach (var (key, value) in obj)
{
json[key] = ParseJson(value);
}
return json;
} }
default: default:
return JsonValue.Create(value); return JsonValue.Create(input);
} }
} }
public override object ParseLiteral(IValue value) public override object ParseLiteral(GraphQLValue value)
{ {
if (value is JsonValueNode jsonGraphType) if (value is JsonValueNode jsonGraphType)
{ {
@ -71,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
return value; return value;
} }
public override IValue ToAST(object? value) public override GraphQLValue ToAST(object? value)
{ {
return new JsonValueNode(ParseJson(value)); return new JsonValueNode(ParseJson(value));
} }

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonNoopGraphType.cs

@ -5,8 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GraphQL.Language.AST;
using GraphQL.Types; using GraphQL.Types;
using GraphQLParser.AST;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{ {
@ -20,9 +20,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
Description = "Unstructured Json object"; Description = "Unstructured Json object";
} }
public override object? ParseLiteral(IValue value) public override object? ParseLiteral(GraphQLValue value)
{ {
return value.Value; return value;
} }
public override object? ParseValue(object? value) public override object? ParseValue(object? value)

11
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs

@ -5,16 +5,21 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GraphQL.Language.AST; using GraphQLParser;
using GraphQLParser.AST;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{ {
internal sealed class JsonValueNode : ValueNode<IJsonValue> internal sealed class JsonValueNode : GraphQLValue
{ {
public override ASTNodeKind Kind => ASTNodeKind.ObjectValue;
public IJsonValue Value { get; }
public JsonValueNode(IJsonValue value) public JsonValueNode(IJsonValue value)
: base(value)
{ {
Value = value;
} }
} }
} }

26
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return new AsyncResolver<TSource, T>(resolver); return new AsyncResolver<TSource, T>(resolver);
} }
private sealed class SyncResolver<TSource, T> : IFieldResolver<T>, IFieldResolver private sealed class SyncResolver<TSource, T> : IFieldResolver
{ {
private readonly Func<TSource, IResolveFieldContext, GraphQLExecutionContext, T> resolver; private readonly Func<TSource, IResolveFieldContext, GraphQLExecutionContext, T> resolver;
@ -44,13 +44,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
this.resolver = resolver; this.resolver = resolver;
} }
public T Resolve(IResolveFieldContext context) public ValueTask<object?> ResolveAsync(IResolveFieldContext context)
{ {
var executionContext = (GraphQLExecutionContext)context.UserContext!; var executionContext = (GraphQLExecutionContext)context.UserContext!;
try try
{ {
return resolver((TSource)context.Source!, context, executionContext); var result = resolver((TSource)context.Source!, context, executionContext);
return new ValueTask<object?>(result);
} }
catch (ValidationException ex) catch (ValidationException ex)
{ {
@ -68,14 +70,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
throw; throw;
} }
} }
object IFieldResolver.Resolve(IResolveFieldContext context)
{
return Resolve(context)!;
}
} }
private sealed class AsyncResolver<TSource, T> : IFieldResolver<Task<T>>, IFieldResolver private sealed class AsyncResolver<TSource, T> : IFieldResolver
{ {
private readonly Func<TSource, IResolveFieldContext, GraphQLExecutionContext, Task<T>> resolver; private readonly Func<TSource, IResolveFieldContext, GraphQLExecutionContext, Task<T>> resolver;
@ -84,13 +81,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
this.resolver = resolver; this.resolver = resolver;
} }
public async Task<T> Resolve(IResolveFieldContext context) public async ValueTask<object?> ResolveAsync(IResolveFieldContext context)
{ {
var executionContext = (GraphQLExecutionContext)context.UserContext!; var executionContext = (GraphQLExecutionContext)context.UserContext!;
try try
{ {
return await resolver((TSource)context.Source!, context, executionContext); var result = await resolver((TSource)context.Source!, context, executionContext);
return result;
} }
catch (ValidationException ex) catch (ValidationException ex)
{ {
@ -108,11 +107,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
throw; throw;
} }
} }
object IFieldResolver.Resolve(IResolveFieldContext context)
{
return Resolve(context)!;
}
} }
} }
} }

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

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using GraphQL; using GraphQL;
using GraphQL.Language.AST;
using GraphQL.Types; using GraphQL.Types;
using GraphQL.Utilities; using GraphQL.Utilities;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
@ -127,16 +126,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static TimeSpan CacheDuration(this IResolveFieldContext context) public static TimeSpan CacheDuration(this IResolveFieldContext context)
{ {
var cacheDirective = context.FieldAst.Directives?.Find("cache"); var cacheDirective = context.GetDirective("cache");
if (cacheDirective != null) if (cacheDirective != null)
{ {
var duration = cacheDirective.Arguments?.ValueFor("duration"); var duration = cacheDirective.GetArgument<int>("duration");
if (duration is IntValue value && value.Value > 0) return TimeSpan.FromSeconds(duration);
{
return TimeSpan.FromSeconds(value.Value);
}
} }
return TimeSpan.Zero; return TimeSpan.Zero;

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

@ -20,8 +20,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="27.2.1" /> <PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="GraphQL" Version="4.7.1" /> <PackageReference Include="GraphQL" Version="5.3.0" />
<PackageReference Include="GraphQL.DataLoader" Version="4.7.1" /> <PackageReference Include="GraphQL.DataLoader" Version="5.3.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

35
backend/src/Squidex.Web/GraphQL/BufferingDocumentWriter.cs

@ -1,35 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.NewtonsoftJson;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json;
namespace Squidex.Web.GraphQL
{
public sealed class BufferingDocumentWriter : IDocumentWriter
{
private readonly DocumentWriter documentWriter;
public BufferingDocumentWriter(Action<JsonSerializerSettings> action)
{
documentWriter = new DocumentWriter(action);
}
public async Task WriteAsync<T>(Stream stream, T value,
CancellationToken cancellationToken = default)
{
await using (var bufferStream = new FileBufferingWriteStream())
{
await documentWriter.WriteAsync(bufferStream, value, cancellationToken);
await bufferStream.DrainBufferAsync(stream, cancellationToken);
}
}
}
}

61
backend/src/Squidex.Web/GraphQL/BufferingGraphQLSerializer.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using Microsoft.AspNetCore.WebUtilities;
namespace Squidex.Web.GraphQL
{
public sealed class BufferingGraphQLSerializer : IGraphQLTextSerializer
{
private readonly IGraphQLTextSerializer inner;
public BufferingGraphQLSerializer(IGraphQLTextSerializer inner)
{
this.inner = inner;
}
public async ValueTask<T?> ReadAsync<T>(Stream stream,
CancellationToken cancellationToken = default)
{
await using (var bufferStream = new FileBufferingReadStream(stream, 30 * 1024))
{
await bufferStream.DrainAsync(cancellationToken);
bufferStream.Seek(0L, SeekOrigin.Begin);
return await inner.ReadAsync<T>(bufferStream, cancellationToken);
}
}
public async Task WriteAsync<T>(Stream stream, T? value,
CancellationToken cancellationToken = default)
{
await using (var bufferStream = new FileBufferingWriteStream())
{
await inner.WriteAsync(bufferStream, value, cancellationToken);
await bufferStream.DrainBufferAsync(stream, cancellationToken);
}
}
public string Serialize<T>(T? value)
{
return inner.Serialize<T>(value);
}
public T? Deserialize<T>(string? value)
{
return inner.Deserialize<T>(value);
}
public T? ReadNode<T>(object? value)
{
return inner.ReadNode<T>(value);
}
}
}

3
backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GraphQL;
using GraphQL.Server.Transports.AspNetCore; using GraphQL.Server.Transports.AspNetCore;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -20,8 +19,6 @@ namespace Squidex.Web.GraphQL
public Task<IDictionary<string, object>> BuildUserContext(HttpContext httpContext) public Task<IDictionary<string, object>> BuildUserContext(HttpContext httpContext)
{ {
var x = httpContext.RequestServices.GetRequiredService<IDocumentWriter>();
var executionContext = (GraphQLExecutionContext)factory(httpContext.RequestServices, new object[] { httpContext.Context() }); var executionContext = (GraphQLExecutionContext)factory(httpContext.RequestServices, new object[] { httpContext.Context() });
return Task.FromResult<IDictionary<string, object>>(executionContext); return Task.FromResult<IDictionary<string, object>>(executionContext);

7
backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GraphQL;
using GraphQL.Server.Transports.AspNetCore; using GraphQL.Server.Transports.AspNetCore;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -14,14 +15,14 @@ namespace Squidex.Web.GraphQL
{ {
private readonly GraphQLHttpMiddleware<DummySchema> middleware; private readonly GraphQLHttpMiddleware<DummySchema> middleware;
public GraphQLRunner(IGraphQLRequestDeserializer deserializer) public GraphQLRunner(IGraphQLTextSerializer deserializer)
{ {
middleware = new GraphQLHttpMiddleware<DummySchema>(x => Task.CompletedTask, deserializer); middleware = new GraphQLHttpMiddleware<DummySchema>(deserializer);
} }
public Task InvokeAsync(HttpContext context) public Task InvokeAsync(HttpContext context)
{ {
return middleware.InvokeAsync(context); return middleware.InvokeAsync(context, x => Task.CompletedTask);
} }
} }
} }

2
backend/src/Squidex.Web/Services/UrlGenerator.cs

@ -44,7 +44,7 @@ namespace Squidex.Web.Services
public string AssetContentCDNBase() public string AssetContentCDNBase()
{ {
return contentOptions.CDN ?? string.Empty; return assetOptions.CDN ?? string.Empty;
} }
public string AssetContentBase() public string AssetContentBase()

5
backend/src/Squidex.Web/Squidex.Web.csproj

@ -17,8 +17,9 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="GraphQL.NewtonsoftJson" Version="4.7.1" /> <PackageReference Include="GraphQL" Version="5.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="5.2.0" /> <PackageReference Include="GraphQL.NewtonsoftJson" Version="5.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="6.1.0" />
<PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" /> <PackageReference Include="Lazy.Fody" Version="1.11.0" PrivateAssets="all" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

24
backend/src/Squidex/Config/Domain/SerializationServices.cs

@ -6,10 +6,6 @@
// ========================================================================== // ==========================================================================
using System.Security.Claims; using System.Security.Claims;
using GraphQL;
using GraphQL.Execution;
using GraphQL.NewtonsoftJson;
using GraphQL.Server;
using Migrations; using Migrations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
@ -30,8 +26,6 @@ using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.Json; using Squidex.Infrastructure.Queries.Json;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Web.GraphQL;
using IGraphQLBuilder = GraphQL.DI.IGraphQLBuilder;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
@ -122,23 +116,5 @@ namespace Squidex.Config.Domain
return builder; return builder;
} }
public static IGraphQLBuilder AddSquidexJson(this IGraphQLBuilder builder)
{
builder.AddDocumentWriter(c =>
{
var errorInfoProvider = c.GetRequiredService<IErrorInfoProvider>();
return new BufferingDocumentWriter(options =>
{
options.ContractResolver = new ExecutionResultContractResolver(errorInfoProvider);
options.Converters.Add(new JsonValueConverter());
options.Converters.Add(new WriteonlyGeoJsonConverter());
});
});
return builder;
}
} }
} }

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

@ -8,6 +8,9 @@
using GraphQL; using GraphQL;
using GraphQL.DataLoader; using GraphQL.DataLoader;
using GraphQL.DI; using GraphQL.DI;
using GraphQL.Execution;
using GraphQL.MicrosoftDI;
using GraphQL.NewtonsoftJson;
using GraphQL.Server; using GraphQL.Server;
using GraphQL.Server.Transports.AspNetCore; using GraphQL.Server.Transports.AspNetCore;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -18,6 +21,7 @@ using Squidex.Config.Domain;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Pipeline.Plugins; using Squidex.Pipeline.Plugins;
using Squidex.Web; using Squidex.Web;
using Squidex.Web.GraphQL; using Squidex.Web.GraphQL;
@ -87,15 +91,13 @@ namespace Squidex.Config.Web
public static void AddSquidexGraphQL(this IServiceCollection services) public static void AddSquidexGraphQL(this IServiceCollection services)
{ {
GraphQL.MicrosoftDI.GraphQLBuilderExtensions.AddGraphQL(services) services.AddGraphQL(builder =>
.AddServer(false, options => {
{ builder.AddApolloTracing();
options.EnableMetrics = false; builder.AddSchema<DummySchema>();
}) builder.AddSquidexJson(); // Use Newtonsoft.JSON for custom converters.
.AddSchema<DummySchema>() builder.AddDataLoader();
.AddSystemTextJson() });
.AddSquidexJson() // Use Newtonsoft.JSON for custom converters.
.AddDataLoader();
services.AddSingletonAs<DummySchema>() services.AddSingletonAs<DummySchema>()
.AsSelf(); .AsSelf();
@ -109,5 +111,21 @@ namespace Squidex.Config.Web
services.AddSingletonAs<GraphQLRunner>() services.AddSingletonAs<GraphQLRunner>()
.AsSelf(); .AsSelf();
} }
private static IGraphQLBuilder AddSquidexJson(this IGraphQLBuilder builder)
{
builder.AddSerializer(c =>
{
var errorInfoProvider = c.GetRequiredService<IErrorInfoProvider>();
return new BufferingGraphQLSerializer(new GraphQLSerializer(options =>
{
options.Converters.Add(new JsonValueConverter());
options.Converters.Add(new WriteonlyGeoJsonConverter());
}));
});
return builder;
}
} }
} }

7
backend/src/Squidex/Squidex.csproj

@ -35,11 +35,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.3" /> <PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.3" />
<PackageReference Include="GraphQL.DataLoader" Version="4.7.1" /> <PackageReference Include="GraphQL" Version="5.3.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="4.7.1" /> <PackageReference Include="GraphQL.MicrosoftDI" Version="5.3.0" />
<PackageReference Include="GraphQL.Server.Core" Version="5.2.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.NewtonsoftJson" Version="5.2.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.SystemTextJson" Version="5.2.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

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

@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var result = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" }); var result = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" });
var json = serializer.Serialize(result, true); var json = serializer.Serialize(result);
Assert.NotEmpty(json); Assert.NotEmpty(json);
} }

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

@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content); commandContext.Complete(content);
var result = await ExecuteAsync(new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsCreate); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsCreate);
var expected = new var expected = new
{ {
@ -264,7 +264,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content); commandContext.Complete(content);
var result = await ExecuteAsync(new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsUpdateOwn);
var expected = new var expected = new
{ {
@ -373,7 +373,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content); commandContext.Complete(content);
var result = await ExecuteAsync(new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpsert); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsUpsert);
var expected = new var expected = new
{ {
@ -482,7 +482,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
commandContext.Complete(content); commandContext.Complete(content);
var result = await ExecuteAsync(new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsUpdateOwn);
var expected = new var expected = new
{ {
@ -763,7 +763,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id) data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id)
}; };
return JObject.FromObject(input).ToInputs(); return serializer.ReadNode<Inputs>(JObject.FromObject(input))!;
} }
} }
} }

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

@ -25,7 +25,7 @@ using Squidex.Domain.Apps.Entities.Contents.TestData;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Users; using Squidex.Shared.Users;
using Xunit; using Xunit;
@ -36,9 +36,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public class GraphQLTestBase : IClassFixture<TranslationsFixture> public class GraphQLTestBase : IClassFixture<TranslationsFixture>
{ {
protected readonly IJsonSerializer serializer = protected readonly GraphQLSerializer serializer = new GraphQLSerializer(options =>
TestUtils.CreateSerializer(TypeNameHandling.None, {
new ExecutionResultJsonConverter(new ErrorInfoProvider())); options.Formatting = Formatting.Indented;
options.Converters.Add(new JsonValueConverter());
options.Converters.Add(new WriteonlyGeoJsonConverter());
});
protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
protected readonly ICommandBus commandBus = A.Fake<ICommandBus>(); protected readonly ICommandBus commandBus = A.Fake<ICommandBus>();
protected readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); protected readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
@ -61,12 +65,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
requestContext = new Context(Mocks.FrontendUser(), TestApp.Default); requestContext = new Context(Mocks.FrontendUser(), TestApp.Default);
} }
protected void AssertResult(object lhs, ExecutionResult result) protected void AssertResult(object expected, ExecutionResult result)
{ {
var rhsJson = serializer.Serialize(result, true); var jsonOutputResult = serializer.Serialize(result);
var lhsJson = serializer.Serialize(lhs, true); var isonOutputExpected = serializer.Serialize(expected);
Assert.Equal(lhsJson, rhsJson); Assert.Equal(isonOutputExpected, jsonOutputResult);
} }
protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, string? permissionId = null) protected Task<ExecutionResult> ExecuteAsync(ExecutionOptions options, string? permissionId = null)
@ -94,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
options.Listeners.Add(listener); options.Listeners.Add(listener);
} }
await sut.ConfigureAsync(options); await sut.ExecuteAsync(options, x => Task.FromResult<ExecutionResult>(null!));
return await new DocumentExecuter().ExecuteAsync(options); return await new DocumentExecuter().ExecuteAsync(options);
} }

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

@ -22,8 +22,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.0" /> <PackageReference Include="FakeItEasy" Version="7.3.0" />
<PackageReference Include="FluentAssertions" Version="6.4.0" /> <PackageReference Include="FluentAssertions" Version="6.4.0" />
<PackageReference Include="GraphQL" Version="4.7.1" /> <PackageReference Include="GraphQL" Version="5.3.0" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="4.7.1" /> <PackageReference Include="GraphQL.NewtonsoftJson" Version="5.3.0" />
<PackageReference Include="Lorem.Universal.Net" Version="4.0.80" /> <PackageReference Include="Lorem.Universal.Net" Version="4.0.80" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.694"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.694">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

Loading…
Cancel
Save