Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL 4 #9087

Merged
merged 43 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
738f448
GraphQL 4
agriffard Apr 7, 2021
f42ac11
started on dataloaders, removed no longer required serviceprovider on…
carlwoodhouse Apr 9, 2021
f2fdd21
updated some more requestservices references
carlwoodhouse Apr 9, 2021
a4c54b7
few type fixes
carlwoodhouse Apr 9, 2021
4d80c30
More fixes
agriffard Apr 9, 2021
58330aa
fix newtonsoft conflicting reference
carlwoodhouse Apr 9, 2021
b86b539
More fixes
agriffard Apr 9, 2021
80c983b
improving middleware
carlwoodhouse Apr 9, 2021
b41bdb3
merge
carlwoodhouse Apr 9, 2021
782f516
actually fixed one test :P
carlwoodhouse Apr 9, 2021
5851eb1
fixed some more tests
carlwoodhouse Apr 9, 2021
ee07c35
.
carlwoodhouse Apr 9, 2021
1501f38
fixed some of the blog integration tests
carlwoodhouse Apr 9, 2021
79d6ce2
fix some tests
carlwoodhouse Apr 9, 2021
c97640f
fixed more tests
carlwoodhouse Apr 9, 2021
257dc55
ResolvedType
agriffard Apr 9, 2021
78d0b52
fix unit tests
carlwoodhouse Apr 9, 2021
54b0195
merge
carlwoodhouse Apr 9, 2021
6bb635b
Merge branch 'dev' into ag/graphql4
sebastienros Apr 11, 2021
3f93e55
Merge branch 'dev' into ag/graphql4
agriffard Apr 20, 2021
2064734
GraphQL 4.3.0
agriffard Apr 20, 2021
a7fde44
GraphQL 4.4.0
agriffard Apr 22, 2021
c0d9715
Merge branch 'dev' into ag/graphql4
agriffard Apr 22, 2021
e193d32
Merge branch 'dev' into ag/graphql4 and use version 4.5.0
agriffard Apr 29, 2021
a37ffaa
Merge branch 'dev' into ag/graphql4
agriffard May 6, 2021
1757f54
graphql 4.6.1
carlwoodhouse Oct 11, 2021
d9a79fe
Merge branch 'main' into ag/graphql4
carlwoodhouse Oct 11, 2021
48bfb93
microsoftdi package ref issue
carlwoodhouse Oct 11, 2021
c68e6ba
fix permissions
carlwoodhouse Oct 11, 2021
8e81024
merge main
carlwoodhouse Nov 30, 2021
cad6999
fix dynamic fields
carlwoodhouse Nov 30, 2021
cc3eb91
Update graphql.net to 4.6.1 (#10782)
MikeKry Jan 10, 2022
760d279
Merge branch 'main' into ag/graphql4
agriffard Jan 14, 2022
58b48dd
Merge branch 'main' into ag/graphql4
agriffard Feb 10, 2022
f9e6b91
Merge branch 'main' into ag/graphql4
agriffard Apr 4, 2022
098106b
GraphQL4 - resolve async write using System.Text.Json (#11499)
MikeKry Apr 28, 2022
d96848a
Merge branch 'main' into ag/graphql4
agriffard Apr 28, 2022
7a5eddc
Merge branch 'main' into ag/graphql4
sebastienros Sep 1, 2022
2ddcc51
Update RequiresPermissionValidationRuleTests.cs
sebastienros Sep 1, 2022
78cf8d6
Update RequiresPermissionValidationRuleTests.cs
sebastienros Sep 1, 2022
89556f6
Reference NewtonSoft.Json
sebastienros Sep 1, 2022
b8eb3ba
Fix build
sebastienros Sep 6, 2022
8d9bc0a
Bump version to 4.8.0
sebastienros Sep 8, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/OrchardCore.Build/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
<PackageManagement Include="Castle.Core" Version="5.1.0" />
<PackageManagement Include="Irony.Core" Version="1.0.7" />
<PackageManagement Include="Fluid.Core" Version="2.2.15" />
<PackageManagement Include="GraphQL" Version="2.4.0" />
<PackageManagement Include="GraphQL" Version="4.8.0" />
<PackageManagement Include="GraphQL.DataLoader" Version="4.8.0" />
<PackageManagement Include="GraphQL.MicrosoftDI" Version="4.8.0" />
<PackageManagement Include="GraphQL.SystemTextJson" Version="4.8.0" />
<PackageManagement Include="Jint" Version="3.0.0-beta-2037" />
<PackageManagement Include="HtmlSanitizer" Version="7.1.542" />
<PackageManagement Include="Lorem.Universal.NET" Version="4.0.80" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using GraphQL;
using Microsoft.AspNetCore.Http;

namespace OrchardCore.Apis.GraphQL
{
internal static class DocumentWriterExtensions
{
public static async Task WriteErrorAsync(this IDocumentWriter documentWriter, HttpContext context, string message, Exception e = null)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var errorResult = new ExecutionResult
{
Errors = new ExecutionErrors()
};

if (e == null)
{
errorResult.Errors.Add(new ExecutionError(message));
}
else
{
errorResult.Errors.Add(new ExecutionError(message, e));
}

context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Response.ContentType = MediaTypeNames.Application.Json;

await documentWriter.WriteAsync(context.Response.Body, errorResult);
}
}
}
134 changes: 53 additions & 81 deletions src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/GraphQLMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Execution;
using GraphQL.SystemTextJson;
using GraphQL.Validation;
using GraphQL.Validation.Complexity;
using Microsoft.AspNetCore.Authentication;
Expand All @@ -25,10 +29,10 @@ public class GraphQLMiddleware
private readonly RequestDelegate _next;
private readonly GraphQLSettings _settings;
private readonly IDocumentExecuter _executer;

internal static readonly Encoding _utf8Encoding = new UTF8Encoding(false);
private readonly static MediaType _jsonMediaType = new MediaType("application/json");
private readonly static MediaType _graphQlMediaType = new MediaType("application/graphql");
private readonly static JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

public GraphQLMiddleware(
RequestDelegate next,
Expand All @@ -40,7 +44,7 @@ public GraphQLMiddleware(
_executer = executer;
}

public async Task Invoke(HttpContext context, IAuthorizationService authorizationService, IAuthenticationService authenticationService, ISchemaFactory schemaService)
public async Task Invoke(HttpContext context, IAuthorizationService authorizationService, IAuthenticationService authenticationService, ISchemaFactory schemaService, IDocumentWriter documentWriter)
{
if (!IsGraphQLRequest(context))
{
Expand All @@ -59,7 +63,7 @@ public async Task Invoke(HttpContext context, IAuthorizationService authorizatio

if (authorized)
{
await ExecuteAsync(context, schemaService);
await ExecuteAsync(context, schemaService, documentWriter);
}
else
{
Expand All @@ -73,85 +77,54 @@ private bool IsGraphQLRequest(HttpContext context)
return context.Request.Path.StartsWithNormalizedSegments(_settings.Path, StringComparison.OrdinalIgnoreCase);
}

private async Task ExecuteAsync(HttpContext context, ISchemaFactory schemaService)
private async Task ExecuteAsync(HttpContext context, ISchemaFactory schemaService, IDocumentWriter documentWriter)
{
var schema = await schemaService.GetSchemaAsync();

GraphQLRequest request = null;

// c.f. https://graphql.org/learn/serving-over-http/#post-request

if (HttpMethods.IsPost(context.Request.Method))
try
{
if (string.IsNullOrEmpty(context.Request.ContentType))
if (HttpMethods.IsPost(context.Request.Method))
{
await WriteErrorAsync(context, "Missing content-type");
return;
}
var mediaType = new MediaType(context.Request.ContentType);

var mediaType = new MediaType(context.Request.ContentType);

try
{
if (mediaType.IsSubsetOf(_jsonMediaType))
{
using (var sr = new StreamReader(context.Request.Body))
{
// Asynchronous read is mandatory.
var json = await sr.ReadToEndAsync();
request = JObject.Parse(json).ToObject<GraphQLRequest>();
}
}
else if (mediaType.IsSubsetOf(_graphQlMediaType))
if (mediaType.IsSubsetOf(_jsonMediaType) || mediaType.IsSubsetOf(_graphQlMediaType))
{
request = new GraphQLRequest();

using (var sr = new StreamReader(context.Request.Body))
{
request.Query = await sr.ReadToEndAsync();
}
}
else if (context.Request.Query.ContainsKey("query"))
{
request = new GraphQLRequest
if (mediaType.IsSubsetOf(_graphQlMediaType))
{
Query = context.Request.Query["query"]
};
using var sr = new StreamReader(context.Request.Body);

if (context.Request.Query.ContainsKey("variables"))
{
request.Variables = JObject.Parse(context.Request.Query["variables"]);
request = new GraphQLRequest
{
Query = await sr.ReadToEndAsync()
};
}

if (context.Request.Query.ContainsKey("operationName"))
else
{
request.OperationName = context.Request.Query["operationName"];
request = await JsonSerializer.DeserializeAsync<GraphQLRequest>(context.Request.Body, _jsonSerializerOptions);
}
}
else
{
await WriteErrorAsync(context, "The request needs a valid content-type or a query argument");
return;
request = CreateRequestFromQueryString(context);
}
}
catch (Exception e)
else if (HttpMethods.IsGet(context.Request.Method))
{
await WriteErrorAsync(context, "An error occurred while processing the GraphQL query", e);
return;
}
}
else if (HttpMethods.IsGet(context.Request.Method))
{
if (!context.Request.Query.ContainsKey("query"))
{
await WriteErrorAsync(context, "The 'query' query string parameter is missing");
return;
request = CreateRequestFromQueryString(context, true);
}

request = new GraphQLRequest
if (request == null)
{
Query = context.Request.Query["query"]
};
throw new InvalidOperationException("Unable to create a graphqlrequest from this request");
}
}
catch (Exception e)
{
await documentWriter.WriteErrorAsync(context, "An error occurred while processing the GraphQL query", e);
return;
}

var queryToExecute = request.Query;
Expand All @@ -167,17 +140,16 @@ private async Task ExecuteAsync(HttpContext context, ISchemaFactory schemaServic
queryToExecute = queries[request.NamedQuery];
}

var schema = await schemaService.GetSchemaAsync();
var dataLoaderDocumentListener = context.RequestServices.GetRequiredService<IDocumentExecutionListener>();

var result = await _executer.ExecuteAsync(_ =>
{
_.Schema = schema;
_.Query = queryToExecute;
_.OperationName = request.OperationName;
_.Inputs = request.Variables.ToInputs();
_.UserContext = _settings.BuildUserContext?.Invoke(context);
_.ExposeExceptions = _settings.ExposeExceptions;
_.ValidationRules = DocumentValidator.CoreRules()
_.ValidationRules = DocumentValidator.CoreRules
.Concat(context.RequestServices.GetServices<IValidationRule>());
_.ComplexityConfiguration = new ComplexityConfiguration
{
Expand All @@ -186,48 +158,48 @@ private async Task ExecuteAsync(HttpContext context, ISchemaFactory schemaServic
FieldImpact = _settings.FieldImpact
};
_.Listeners.Add(dataLoaderDocumentListener);
_.RequestServices = context.RequestServices;
});

context.Response.StatusCode = (int)(result.Errors == null || result.Errors.Count == 0
? HttpStatusCode.OK
: result.Errors.Any(x => x.Code == RequiresPermissionValidationRule.ErrorCode)
: result.Errors.Any(x => x is ValidationError ve && ve.Number == RequiresPermissionValidationRule.ErrorCode)
? HttpStatusCode.Unauthorized
: HttpStatusCode.BadRequest);

context.Response.ContentType = "application/json";
context.Response.ContentType = MediaTypeNames.Application.Json;

// Asynchronous write to the response body is mandatory.
var encodedBytes = _utf8Encoding.GetBytes(JObject.FromObject(result).ToString());
await context.Response.Body.WriteAsync(encodedBytes, 0, encodedBytes.Length);
await documentWriter.WriteAsync(context.Response.Body, result);
}

private async Task WriteErrorAsync(HttpContext context, string message, Exception e = null)
private static GraphQLRequest CreateRequestFromQueryString(HttpContext context, bool validateQueryKey = false)
{
if (message == null)
if (!context.Request.Query.ContainsKey("query"))
{
throw new ArgumentNullException(nameof(message));
if (validateQueryKey)
{
throw new InvalidOperationException("The 'query' query string parameter is missing");
}

return null;
}

var errorResult = new ExecutionResult
var request = new GraphQLRequest
{
Errors = new ExecutionErrors()
Query = context.Request.Query["query"]
};

if (e == null)
if (context.Request.Query.ContainsKey("variables"))
{
errorResult.Errors.Add(new ExecutionError(message));
request.Variables = JsonSerializer.Deserialize<JsonElement>(context.Request.Query["variables"], _jsonSerializerOptions);
}
else

if (context.Request.Query.ContainsKey("operationName"))
{
errorResult.Errors.Add(new ExecutionError(message, e));
request.OperationName = context.Request.Query["operationName"];
}

context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Response.ContentType = "application/json";

// Asynchronous write to the response body is mandatory.
var encodedBytes = _utf8Encoding.GetBytes(JObject.FromObject(errorResult).ToString());
await context.Response.Body.WriteAsync(encodedBytes, 0, encodedBytes.Length);
return request;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq;
using System.Text.Json;
using GraphQL;

namespace OrchardCore.Apis.GraphQL
{
Expand All @@ -7,6 +8,7 @@ public class GraphQLRequest
public string OperationName { get; set; }
public string NamedQuery { get; set; }
public string Query { get; set; }
public JObject Variables { get; set; }

public JsonElement Variables { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="GraphQL" />
<PackageReference Include="GraphQL.DataLoader" />
<PackageReference Include="GraphQL.MicrosoftDI" />
<PackageReference Include="GraphQL.SystemTextJson" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="YesSql.Abstractions" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
using System;
using GraphQL;
using GraphQL.Conversion;
using GraphQL.Types;

namespace OrchardCore.Apis.GraphQL
{
public class OrchardFieldNameConverter : IFieldNameConverter
public class OrchardFieldNameConverter : INameConverter
{
private readonly IFieldNameConverter _defaultConverter = new CamelCaseFieldNameConverter();
private readonly INameConverter _defaultConverter = new CamelCaseNameConverter();

public string NameFor(string field, Type parentType)
// todo: custom argument name?
public string NameForArgument(string argumentName, IComplexGraphType parentGraphType, FieldType field)
{
var attributes = parentType?.GetCustomAttributes(typeof(GraphQLFieldNameAttribute), true);
carlwoodhouse marked this conversation as resolved.
Show resolved Hide resolved
return _defaultConverter.NameForArgument(argumentName, parentGraphType, field);
}

// TODO: check functionality
public string NameForField(string fieldName, IComplexGraphType parentGraphType)
{
var attributes = parentGraphType?.GetType().GetCustomAttributes(typeof(GraphQLFieldNameAttribute), true);

if (attributes != null)
{
foreach (GraphQLFieldNameAttribute attribute in attributes)
{
if (attribute.Field == field)
if (attribute.Field == fieldName)
{
return attribute.Mapped;
}
}
}

return _defaultConverter.NameFor(field, parentType);
return _defaultConverter.NameForField(fieldName, parentGraphType);
}
}
}
Loading