Skip to content

Commit

Permalink
Support persisted documents (#1147)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane32 authored Aug 23, 2024
1 parent cbafeed commit 61f46a0
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 55 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<VersionPrefix>8.0.0-preview</VersionPrefix>
<VersionPrefix>8.0.1-preview</VersionPrefix>
<NextVersion>9.0.0</NextVersion>
<LangVersion>latest</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down Expand Up @@ -32,7 +32,7 @@
<RootNamespace>GraphQL.Server.$(MSBuildProjectName)</RootNamespace>
<PackageId>GraphQL.Server.$(MSBuildProjectName)</PackageId>

<GraphQLVersion>[8.0.0,9.0.0)</GraphQLVersion>
<GraphQLVersion>[8.0.1,9.0.0)</GraphQLVersion>

<SignAssembly>true</SignAssembly>
<_FriendAssembliesPublicKey>PublicKey=0024000004800000940000000602000000240000525341310004000001000100352162dbf27be78fc45136884b8f324aa9f1dfc928c96c24704bf1df1a8779b2f26c760ed8321eca5b95ea6bd9bb60cd025b300f73bd1f4ae1ee6e281f85c527fa013ab5cb2c3fc7a1cbef7f9bf0c9014152e6a21f6e0ac6a371f8b45c6d7139c9119df9eeecf1cf59063545bb7c07437b1bc12be2c57d108d72d6c27176fbb8</_FriendAssembliesPublicKey>
Expand Down
11 changes: 8 additions & 3 deletions src/Transports.AspNetCore/GraphQLHttpMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public class GraphQLHttpMiddleware : IUserContextBuilder
private const string VARIABLES_KEY = "variables";
private const string EXTENSIONS_KEY = "extensions";
private const string OPERATION_NAME_KEY = "operationName";
private const string DOCUMENT_ID_KEY = "documentId";
private const string OPERATIONS_KEY = "operations"; // used for multipart/form-data requests per https://github.com/jaydenseric/graphql-multipart-request-spec
private const string MAP_KEY = "map"; // used for multipart/form-data requests per https://github.com/jaydenseric/graphql-multipart-request-spec
private const string MEDIATYPE_GRAPHQLJSON = "application/graphql+json"; // deprecated
Expand Down Expand Up @@ -199,7 +200,8 @@ protected virtual async Task InvokeAsync(HttpContext context, RequestDelegate ne
Query = urlGQLRequest?.Query ?? bodyGQLRequest?.Query,
Variables = urlGQLRequest?.Variables ?? bodyGQLRequest?.Variables,
Extensions = urlGQLRequest?.Extensions ?? bodyGQLRequest?.Extensions,
OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName
OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName,
DocumentId = urlGQLRequest?.DocumentId ?? bodyGQLRequest?.DocumentId,
};

await HandleRequestAsync(context, next, gqlRequest);
Expand Down Expand Up @@ -343,8 +345,8 @@ void ApplyMapToRequests(Dictionary<string, string?[]> map, IFormCollection form,
foreach (var entry in map)
{
// validate entry key
if (entry.Key == "" || entry.Key == "query" || entry.Key == "operationName" || entry.Key == "variables" || entry.Key == "extensions" || entry.Key == "operations" || entry.Key == "map")
throw new InvalidMapError("Map key cannot be query, operationName, variables, extensions, operations or map.");
if (entry.Key == "" || entry.Key == QUERY_KEY || entry.Key == OPERATION_NAME_KEY || entry.Key == VARIABLES_KEY || entry.Key == EXTENSIONS_KEY || entry.Key == DOCUMENT_ID_KEY || entry.Key == OPERATIONS_KEY || entry.Key == MAP_KEY)
throw new InvalidMapError("Map key cannot be query, operationName, variables, extensions, documentId, operations or map.");
// locate file
var file = form.Files[entry.Key]
?? throw new InvalidMapError("Map key does not refer to an uploaded file.");
Expand Down Expand Up @@ -683,6 +685,7 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
Query = request?.Query,
Variables = request?.Variables,
Extensions = request?.Extensions,
DocumentId = request?.DocumentId,
CancellationToken = context.RequestAborted,
OperationName = request?.OperationName,
RequestServices = serviceProvider,
Expand Down Expand Up @@ -1212,6 +1215,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo
Variables = _options.ReadVariablesFromQueryString && queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize<Inputs>(variablesValues[0]) : null,
Extensions = _options.ReadExtensionsFromQueryString && queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize<Inputs>(extensionsValues[0]) : null,
OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null,
DocumentId = queryCollection.TryGetValue(DOCUMENT_ID_KEY, out var documentIdValues) ? documentIdValues[0] : null,
};

private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new()
Expand All @@ -1220,6 +1224,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo
Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize<Inputs>(variablesValues[0]) : null,
Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize<Inputs>(extensionsValues[0]) : null,
OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null,
DocumentId = formCollection.TryGetValue(DOCUMENT_ID_KEY, out var documentIdValues) ? documentIdValues[0] : null,
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ protected override async Task<ExecutionResult> ExecuteRequestAsync(OperationMess
Query = request.Query,
Variables = request.Variables,
Extensions = request.Extensions,
DocumentId = request.DocumentId,
OperationName = request.OperationName,
RequestServices = scope.ServiceProvider,
CancellationToken = CancellationToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ protected override async Task<ExecutionResult> ExecuteRequestAsync(OperationMess
Query = request.Query,
Variables = request.Variables,
Extensions = request.Extensions,
DocumentId = request.DocumentId,
OperationName = request.OperationName,
RequestServices = scope.ServiceProvider,
CancellationToken = CancellationToken,
Expand Down
59 changes: 48 additions & 11 deletions tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
using GraphQL.PersistedDocuments;
using GraphQL.Server.Transports.AspNetCore.Errors;
using GraphQL.Validation;

Expand All @@ -10,6 +11,7 @@ public class GetTests : IDisposable
private GraphQLHttpMiddlewareOptions _options = null!;
private GraphQLHttpMiddlewareOptions _options2 = null!;
private Action<ExecutionOptions> _configureExecution = _ => { };
private bool _enablePersistedDocuments = true;
private readonly TestServer _server;

public GetTests()
Expand All @@ -18,13 +20,35 @@ public GetTests()
hostBuilder.ConfigureServices(services =>
{
services.AddSingleton<Chat.IChat, Chat.Chat>();
services.AddGraphQL(b => b
.AddAutoSchema<Chat.Query>(s => s
.WithMutation<Chat.Mutation>()
.WithSubscription<Chat.Subscription>())
.AddSchema<Schema2>()
.AddSystemTextJson()
.ConfigureExecutionOptions(o => _configureExecution(o)));
services.AddGraphQL(b =>
{
b
.AddAutoSchema<Chat.Query>(s => s
.WithMutation<Chat.Mutation>()
.WithSubscription<Chat.Subscription>())
.AddSchema<Schema2>()
.AddSystemTextJson()
.ConfigureExecution((options, next) =>
{
if (_enablePersistedDocuments)
{
var handler = options.RequestServices!.GetRequiredService<PersistedDocumentHandler>();
return handler.ExecuteAsync(options, next);
}
return next(options);
})
.ConfigureExecutionOptions(o => _configureExecution(o));
b.Services.Configure<PersistedDocumentOptions>(o =>
{
o.AllowOnlyPersistedDocuments = false;
o.AllowedPrefixes.Add("test");
o.GetQueryDelegate = (options, prefix, payload) =>
prefix == "test" && payload == "abc" ? new("{count}") :
prefix == "test" && payload == "form" ? new("query op1{ext} query op2($test:String!){ext var(test:$test)}") :
default;
});
});
services.AddSingleton<PersistedDocumentHandler>();
#if NETCOREAPP2_1 || NET48
services.AddHostApplicationLifetime();
#endif
Expand Down Expand Up @@ -78,6 +102,14 @@ public async Task BasicTest()
await response.ShouldBeAsync("""{"data":{"count":0}}""");
}

[Fact]
public async Task PersistedDocumentTest()
{
var client = _server.CreateClient();
using var response = await client.GetAsync("/graphql?documentId=test:abc");
await response.ShouldBeAsync("""{"data":{"count":0}}""");
}

[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
Expand Down Expand Up @@ -346,14 +378,19 @@ public async Task QueryParseError(bool badRequest)
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task NoQuery(bool badRequest)
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(true, true)]
public async Task NoQuery(bool badRequest, bool usePersistedDocumentHandler)
{
_enablePersistedDocuments = usePersistedDocumentHandler;
_options.ValidationErrorsReturnBadRequest = badRequest;
var client = _server.CreateClient();
using var response = await client.GetAsync("/graphql");
await response.ShouldBeAsync(badRequest, """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}""");
await response.ShouldBeAsync(badRequest, usePersistedDocumentHandler
? """{"errors":[{"message":"The request must have a documentId parameter.","extensions":{"code":"DOCUMENT_ID_MISSING","codes":["DOCUMENT_ID_MISSING"]}}]}"""
: """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}""");
}

[Theory]
Expand Down
Loading

0 comments on commit 61f46a0

Please sign in to comment.