Skip to content

Commit eb6046d

Browse files
authored
Add persisted document support (#71)
1 parent 51ee008 commit eb6046d

File tree

8 files changed

+165
-55
lines changed

8 files changed

+165
-55
lines changed

Directory.Build.props

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22

33
<PropertyGroup>
4-
<VersionPrefix>6.0.0-preview</VersionPrefix>
4+
<VersionPrefix>6.0.1-preview</VersionPrefix>
55
<LangVersion>12.0</LangVersion>
66
<Copyright>Shane Krueger</Copyright>
77
<Authors>Shane Krueger</Authors>
@@ -20,7 +20,7 @@
2020
<ImplicitUsings>true</ImplicitUsings>
2121
<Nullable>enable</Nullable>
2222
<IsPackable>false</IsPackable>
23-
<GraphQLVersion>8.0.0</GraphQLVersion>
23+
<GraphQLVersion>8.0.1</GraphQLVersion>
2424
<NoWarn>$(NoWarn);IDE0056;IDE0057;NU1902;NU1903</NoWarn>
2525
</PropertyGroup>
2626

src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class GraphQLHttpMiddleware : IUserContextBuilder
6767
private const string VARIABLES_KEY = "variables";
6868
private const string EXTENSIONS_KEY = "extensions";
6969
private const string OPERATION_NAME_KEY = "operationName";
70+
private const string DOCUMENT_ID_KEY = "documentId";
7071
private const string OPERATIONS_KEY = "operations"; // used for multipart/form-data requests per https://github.com/jaydenseric/graphql-multipart-request-spec
7172
private const string MAP_KEY = "map"; // used for multipart/form-data requests per https://github.com/jaydenseric/graphql-multipart-request-spec
7273
private const string MEDIATYPE_GRAPHQLJSON = "application/graphql+json";
@@ -174,7 +175,8 @@ public virtual async Task InvokeAsync(HttpContext context)
174175
Query = urlGQLRequest?.Query ?? bodyGQLRequest?.Query,
175176
Variables = urlGQLRequest?.Variables ?? bodyGQLRequest?.Variables,
176177
Extensions = urlGQLRequest?.Extensions ?? bodyGQLRequest?.Extensions,
177-
OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName
178+
OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName,
179+
DocumentId = urlGQLRequest?.DocumentId ?? bodyGQLRequest?.DocumentId,
178180
};
179181

180182
await HandleRequestAsync(context, _next, gqlRequest);
@@ -299,8 +301,8 @@ void ApplyMapToRequests(Dictionary<string, string?[]> map, IFormCollection form,
299301

300302
foreach (var entry in map) {
301303
// validate entry key
302-
if (entry.Key == "" || entry.Key == "query" || entry.Key == "operationName" || entry.Key == "variables" || entry.Key == "extensions" || entry.Key == "operations" || entry.Key == "map")
303-
throw new InvalidMapError("Map key cannot be query, operationName, variables, extensions, operations or map.");
304+
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)
305+
throw new InvalidMapError("Map key cannot be query, operationName, variables, extensions, documentId, operations or map.");
304306
// locate file
305307
var file = form.Files[entry.Key]
306308
?? throw new InvalidMapError("Map key does not refer to an uploaded file.");
@@ -603,6 +605,7 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
603605
Query = request?.Query,
604606
Variables = request?.Variables,
605607
Extensions = request?.Extensions,
608+
DocumentId = request?.DocumentId,
606609
CancellationToken = context.RequestAborted,
607610
OperationName = request?.OperationName,
608611
RequestServices = serviceProvider,
@@ -884,13 +887,15 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo
884887
Variables = _options.ReadVariablesFromQueryString && queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize<Inputs>(variablesValues[0]) : null,
885888
Extensions = _options.ReadExtensionsFromQueryString && queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize<Inputs>(extensionsValues[0]) : null,
886889
OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null,
890+
DocumentId = queryCollection.TryGetValue(DOCUMENT_ID_KEY, out var documentIdValues) ? documentIdValues[0] : null,
887891
};
888892

889893
private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new() {
890894
Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null,
891895
Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize<Inputs>(variablesValues[0]) : null,
892896
Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize<Inputs>(extensionsValues[0]) : null,
893897
OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null,
898+
DocumentId = formCollection.TryGetValue(DOCUMENT_ID_KEY, out var documentIdValues) ? documentIdValues[0] : null,
894899
};
895900

896901
/// <summary>

src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs

+1
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ protected override async Task<ExecutionResult> ExecuteRequestAsync(OperationMess
192192
Query = request.Query,
193193
Variables = request.Variables,
194194
Extensions = request.Extensions,
195+
DocumentId = request.DocumentId,
195196
OperationName = request.OperationName,
196197
RequestServices = scope.ServiceProvider,
197198
CancellationToken = CancellationToken,

src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs

+1
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ protected override async Task<ExecutionResult> ExecuteRequestAsync(OperationMess
172172
Query = request.Query,
173173
Variables = request.Variables,
174174
Extensions = request.Extensions,
175+
DocumentId = request.DocumentId,
175176
OperationName = request.OperationName,
176177
RequestServices = scope.ServiceProvider,
177178
CancellationToken = CancellationToken,

src/Tests/Middleware/GetTests.cs

+44-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net;
2+
using GraphQL.PersistedDocuments;
23

34
namespace Tests.Middleware;
45

@@ -7,20 +8,39 @@ public class GetTests : IDisposable
78
private GraphQLHttpMiddlewareOptions _options = null!;
89
private GraphQLHttpMiddlewareOptions _options2 = null!;
910
private readonly Action<ExecutionOptions> _configureExecution = _ => { };
11+
private bool _enablePersistedDocuments = true;
1012
private readonly TestServer _server;
1113

1214
public GetTests()
1315
{
1416
var hostBuilder = new WebHostBuilder();
1517
hostBuilder.ConfigureServices(services => {
1618
services.AddSingleton<Chat.Services.ChatService>();
17-
services.AddGraphQL(b => b
18-
.AddAutoSchema<Chat.Schema.Query>(s => s
19-
.WithMutation<Chat.Schema.Mutation>()
20-
.WithSubscription<Chat.Schema.Subscription>())
21-
.AddSchema<Schema2>()
22-
.AddSystemTextJson()
23-
.ConfigureExecutionOptions(o => _configureExecution(o)));
19+
services.AddGraphQL(b => {
20+
b
21+
.AddAutoSchema<Chat.Schema.Query>(s => s
22+
.WithMutation<Chat.Schema.Mutation>()
23+
.WithSubscription<Chat.Schema.Subscription>())
24+
.AddSchema<Schema2>()
25+
.AddSystemTextJson()
26+
.ConfigureExecution((options, next) => {
27+
if (_enablePersistedDocuments) {
28+
var handler = options.RequestServices!.GetRequiredService<PersistedDocumentHandler>();
29+
return handler.ExecuteAsync(options, next);
30+
}
31+
return next(options);
32+
})
33+
.ConfigureExecutionOptions(o => _configureExecution(o));
34+
b.Services.Configure<PersistedDocumentOptions>(o => {
35+
o.AllowOnlyPersistedDocuments = false;
36+
o.AllowedPrefixes.Add("test");
37+
o.GetQueryDelegate = (options, prefix, payload) =>
38+
prefix == "test" && payload == "abc" ? new("{count}") :
39+
prefix == "test" && payload == "form" ? new("query op1{ext} query op2($test:String!){ext var(test:$test)}") :
40+
default;
41+
});
42+
});
43+
services.AddSingleton<PersistedDocumentHandler>();
2444
#if NETCOREAPP2_1 || NET48
2545
services.AddHostApplicationLifetime();
2646
#endif
@@ -63,6 +83,14 @@ public async Task BasicTest()
6383
await response.ShouldBeAsync(@"{""data"":{""count"":0}}");
6484
}
6585

86+
[Fact]
87+
public async Task PersistedDocumentTest()
88+
{
89+
var client = _server.CreateClient();
90+
using var response = await client.GetAsync("/graphql?documentId=test:abc");
91+
await response.ShouldBeAsync("""{"data":{"count":0}}""");
92+
}
93+
6694
[Theory]
6795
[InlineData(true, true)]
6896
[InlineData(true, false)]
@@ -160,14 +188,19 @@ public async Task QueryParseError(bool badRequest)
160188
}
161189

162190
[Theory]
163-
[InlineData(false)]
164-
[InlineData(true)]
165-
public async Task NoQuery(bool badRequest)
191+
[InlineData(false, false)]
192+
[InlineData(true, false)]
193+
[InlineData(false, true)]
194+
[InlineData(true, true)]
195+
public async Task NoQuery(bool badRequest, bool usePersistedDocumentHandler)
166196
{
197+
_enablePersistedDocuments = usePersistedDocumentHandler;
167198
_options.ValidationErrorsReturnBadRequest = badRequest;
168199
var client = _server.CreateClient();
169200
using var response = await client.GetAsync("/graphql");
170-
await response.ShouldBeAsync(badRequest, @"{""errors"":[{""message"":""GraphQL query is missing."",""extensions"":{""code"":""QUERY_MISSING"",""codes"":[""QUERY_MISSING""]}}]}");
201+
await response.ShouldBeAsync(badRequest, usePersistedDocumentHandler
202+
? """{"errors":[{"message":"The request must have a documentId parameter.","extensions":{"code":"DOCUMENT_ID_MISSING","codes":["DOCUMENT_ID_MISSING"]}}]}"""
203+
: """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}""");
171204
}
172205

173206
[Theory]

0 commit comments

Comments
 (0)