Skip to content

Commit ae55a75

Browse files
committed
Add PreparsedDocumentProvider support
1 parent a15fd29 commit ae55a75

File tree

7 files changed

+227
-12
lines changed

7 files changed

+227
-12
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,42 @@ name mappings that should help to cover more corner cases.
231231

232232

233233

234+
[[execution-graphqlsource-preparsed-document-provider]]
235+
==== PreparsedDocumentProvider
236+
237+
You can configure a `PreparsedDocumentProvider` in `GraphQlSource.Builder` to implement `Document` caching and checks.
238+
Doing so makes it possible to skip the parsing and validation steps of execution entirely. It may also assist you
239+
in implementing query whitelisting.
240+
241+
A simple caching implementation using Caffeine would look something like the following:
242+
243+
[source,java,indent=0,subs="verbatim,quotes"]
244+
----
245+
public class CachingPreparsedDocumentProvider implements PreparsedDocumentProvider {
246+
247+
private final Cache<String, PreparsedDocumentEntry> cache = Caffeine
248+
.newBuilder()
249+
.maximumSize(2500)
250+
.build();
251+
252+
@Override
253+
public PreparsedDocumentEntry getDocument(ExecutionInput executionInput,
254+
Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {
255+
return cache.get(executionInput.getQuery(), queryKey -> parseAndValidateFunction.apply(executionInput));
256+
}
257+
258+
}
259+
----
260+
261+
Please note that caching in the preceding snippet only works when you parameterize your operation using variables:
262+
[source,graphql,indent=0,subs="verbatim,quotes"]
263+
----
264+
query HelloTo($to: String!) {
265+
sayHello(to: $to) {
266+
greeting
267+
}
268+
}
269+
----
234270

235271
[[execution-reactive-datafetcher]]
236272
=== Reactive `DataFetcher`

spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,10 @@
1616

1717
package org.springframework.graphql.execution;
1818

19-
import java.io.IOException;
20-
import java.io.InputStream;
21-
import java.util.ArrayList;
22-
import java.util.Arrays;
23-
import java.util.Collections;
24-
import java.util.List;
25-
import java.util.Map;
26-
import java.util.function.BiFunction;
27-
import java.util.function.Consumer;
28-
2919
import graphql.GraphQL;
3020
import graphql.execution.instrumentation.ChainedInstrumentation;
3121
import graphql.execution.instrumentation.Instrumentation;
22+
import graphql.execution.preparsed.PreparsedDocumentProvider;
3223
import graphql.language.InterfaceTypeDefinition;
3324
import graphql.language.UnionTypeDefinition;
3425
import graphql.schema.GraphQLCodeRegistry;
@@ -40,11 +31,21 @@
4031
import graphql.schema.idl.SchemaGenerator;
4132
import graphql.schema.idl.SchemaParser;
4233
import graphql.schema.idl.TypeDefinitionRegistry;
43-
4434
import org.springframework.core.io.Resource;
35+
import org.springframework.graphql.execution.preparsed.SpringNoOpPreparsedDocumentProvider;
4536
import org.springframework.lang.Nullable;
4637
import org.springframework.util.Assert;
4738

39+
import java.io.IOException;
40+
import java.io.InputStream;
41+
import java.util.ArrayList;
42+
import java.util.Arrays;
43+
import java.util.Collections;
44+
import java.util.List;
45+
import java.util.Map;
46+
import java.util.function.BiFunction;
47+
import java.util.function.Consumer;
48+
4849
/**
4950
* Default implementation of {@link GraphQlSource.Builder} that initializes a
5051
* {@link GraphQL} instance and wraps it with a {@link GraphQlSource} that returns it.
@@ -67,12 +68,15 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
6768

6869
private final List<Instrumentation> instrumentations = new ArrayList<>();
6970

71+
@Nullable
72+
private PreparsedDocumentProvider preparsedDocumentProvider;
73+
7074
@Nullable
7175
private BiFunction<TypeDefinitionRegistry, RuntimeWiring, GraphQLSchema> schemaFactory;
7276

7377
private Consumer<GraphQL.Builder> graphQlConfigurers = (builder) -> {
7478
};
75-
79+
7680

7781
@Override
7882
public GraphQlSource.Builder schemaResources(Resource... resources) {
@@ -92,6 +96,12 @@ public GraphQlSource.Builder defaultTypeResolver(TypeResolver typeResolver) {
9296
return this;
9397
}
9498

99+
@Override
100+
public GraphQlSource.Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) {
101+
this.preparsedDocumentProvider = preparsedDocumentProvider;
102+
return this;
103+
}
104+
95105
@Override
96106
public GraphQlSource.Builder exceptionResolvers(List<DataFetcherExceptionResolver> resolvers) {
97107
this.exceptionResolvers.addAll(resolvers);
@@ -148,6 +158,12 @@ public GraphQlSource build() {
148158
builder = builder.instrumentation(new ChainedInstrumentation(this.instrumentations));
149159
}
150160

161+
PreparsedDocumentProvider preparsedDocumentProvider = (this.preparsedDocumentProvider != null ?
162+
this.preparsedDocumentProvider :
163+
SpringNoOpPreparsedDocumentProvider.INSTANCE);
164+
165+
builder = builder.preparsedDocumentProvider(preparsedDocumentProvider);
166+
151167
this.graphQlConfigurers.accept(builder);
152168
GraphQL graphQl = builder.build();
153169

spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323

2424
import graphql.GraphQL;
2525
import graphql.execution.instrumentation.Instrumentation;
26+
import graphql.execution.preparsed.PreparsedDocumentProvider;
27+
import graphql.language.Document;
2628
import graphql.schema.GraphQLSchema;
2729
import graphql.schema.GraphQLTypeVisitor;
2830
import graphql.schema.TypeResolver;
2931
import graphql.schema.idl.RuntimeWiring;
3032
import graphql.schema.idl.TypeDefinitionRegistry;
3133

3234
import org.springframework.core.io.Resource;
35+
import org.springframework.graphql.execution.preparsed.SpringNoOpPreparsedDocumentProvider;
3336

3437
/**
3538
* Strategy to resolve the {@link GraphQL} instance to use.
@@ -109,6 +112,22 @@ interface Builder {
109112
*/
110113
Builder defaultTypeResolver(TypeResolver typeResolver);
111114

115+
/**
116+
* Configure the {@link PreparsedDocumentProvider} to use for GraphQL requests.
117+
* <p>
118+
* A {@code PreparsedDocumentProvider} can be used to cache and/or whitelist
119+
* {@link Document} instances for queries. Configuring a
120+
* {@code PreparsedDocumentProvider} gives you the ability to skip query parsing
121+
* and validation.
122+
* <p>
123+
* By default, this is set to {@link SpringNoOpPreparsedDocumentProvider}, which
124+
* calls the {@code parseAndValidateFunction}, and does nothing else.
125+
* @param preparsedDocumentProvider the {@code PreparsedDocumentProvider} to use
126+
* @return the current builder
127+
* @see GraphQL#getPreparsedDocumentProvider()
128+
*/
129+
Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider);
130+
112131
/**
113132
* Add {@link DataFetcherExceptionResolver}'s to use for resolving exceptions from
114133
* {@link graphql.schema.DataFetcher}'s.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.springframework.graphql.execution.preparsed;
2+
3+
import graphql.ExecutionInput;
4+
import graphql.execution.preparsed.PreparsedDocumentEntry;
5+
import graphql.execution.preparsed.PreparsedDocumentProvider;
6+
7+
import java.util.function.Function;
8+
9+
/**
10+
* A {@link PreparsedDocumentProvider} calling the {@code parseAndValidateFunction}, and doing nothing else.
11+
*/
12+
public final class SpringNoOpPreparsedDocumentProvider implements PreparsedDocumentProvider {
13+
14+
public static final SpringNoOpPreparsedDocumentProvider INSTANCE = new SpringNoOpPreparsedDocumentProvider();
15+
16+
private SpringNoOpPreparsedDocumentProvider() {
17+
}
18+
19+
@Override
20+
public PreparsedDocumentEntry getDocument(ExecutionInput executionInput,
21+
Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {
22+
return parseAndValidateFunction.apply(executionInput);
23+
}
24+
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.springframework.graphql.execution.preparsed;
2+
3+
import graphql.ExecutionInput;
4+
import graphql.GraphQL;
5+
import graphql.execution.preparsed.PreparsedDocumentEntry;
6+
import graphql.execution.preparsed.PreparsedDocumentProvider;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.graphql.GraphQlSetup;
9+
import org.springframework.graphql.execution.GraphQlSource;
10+
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.ConcurrentMap;
13+
import java.util.function.Function;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
/**
18+
* Tests for
19+
* {@link GraphQlSource.Builder#preparsedDocumentProvider(PreparsedDocumentProvider)}.
20+
*/
21+
public class PreparsedDocumentProviderTests {
22+
23+
private static final String recursiveTestSchema = "type Query { query: Query! }";
24+
25+
private static class CountingPreparsedDocumentProvider implements PreparsedDocumentProvider {
26+
27+
// <query, counter>
28+
private final ConcurrentMap<String, Integer> counter;
29+
30+
private CountingPreparsedDocumentProvider() {
31+
this.counter = new ConcurrentHashMap<>();
32+
}
33+
34+
@Override
35+
public PreparsedDocumentEntry getDocument(ExecutionInput executionInput,
36+
Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {
37+
counter.compute(executionInput.getQuery(), (k, v) -> v == null ? 1 : v + 1);
38+
return parseAndValidateFunction.apply(executionInput);
39+
}
40+
41+
public int getCount(String query) {
42+
return counter.getOrDefault(query, 0);
43+
}
44+
45+
}
46+
47+
@Test
48+
public void correctDocumentProviderIsSet() {
49+
PreparsedDocumentProvider sample = new CountingPreparsedDocumentProvider();
50+
GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).preparsedDocumentProvider(sample).toGraphQl();
51+
assertThat(graphQL.getPreparsedDocumentProvider()).isEqualTo(sample);
52+
}
53+
54+
@Test
55+
public void noOpProviderIsSetByDefault() {
56+
GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).toGraphQl();
57+
assertThat(graphQL.getPreparsedDocumentProvider()).isInstanceOf(SpringNoOpPreparsedDocumentProvider.class);
58+
}
59+
60+
@Test
61+
public void preparsedDocumentProviderWorks() {
62+
CountingPreparsedDocumentProvider sample = new CountingPreparsedDocumentProvider();
63+
GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).preparsedDocumentProvider(sample).toGraphQl();
64+
graphQL.execute("{query }");
65+
graphQL.execute("{query }");
66+
graphQL.execute("{query}");
67+
graphQL.execute("{query }");
68+
graphQL.execute("{query }");
69+
graphQL.execute("{query }");
70+
graphQL.execute("{query }");
71+
graphQL.execute("{query}");
72+
graphQL.execute("{query }");
73+
assertThat(sample.getCount("{query}")).isEqualTo(2);
74+
assertThat(sample.getCount("{query }")).isEqualTo(3);
75+
assertThat(sample.getCount("{query }")).isEqualTo(4);
76+
assertThat(sample.getCount("{ query }")).isEqualTo(0);
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.springframework.graphql.execution.preparsed;
2+
3+
import graphql.ExecutionInput;
4+
import graphql.execution.preparsed.PreparsedDocumentEntry;
5+
import graphql.execution.preparsed.PreparsedDocumentProvider;
6+
import graphql.language.Document;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.time.Duration;
10+
import java.util.function.Function;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
/**
15+
* Tests for {@link SpringNoOpPreparsedDocumentProvider}.
16+
*/
17+
public class SpringNoOpPreparsedDocumentProviderTests {
18+
19+
@Test
20+
public void noOpDocumentProviderAppliesFunction() {
21+
SpringNoOpPreparsedDocumentProvider documentProvider = SpringNoOpPreparsedDocumentProvider.INSTANCE;
22+
PreparsedDocumentEntry documentEntry = new PreparsedDocumentEntry(Document.newDocument().build());
23+
PreparsedDocumentEntry providerResult = documentProvider
24+
.getDocument(ExecutionInput.newExecutionInput("{}").build(), executionInput -> documentEntry);
25+
26+
assertThat(documentEntry).isEqualTo(providerResult);
27+
}
28+
29+
@Test
30+
public void springNoOpDocumentProviderInstanceIsNotNull() {
31+
assertThat(SpringNoOpPreparsedDocumentProvider.INSTANCE).isNotNull();
32+
}
33+
34+
}

spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222

2323
import graphql.GraphQL;
24+
import graphql.execution.preparsed.PreparsedDocumentProvider;
2425
import graphql.schema.DataFetcher;
2526
import graphql.schema.GraphQLTypeVisitor;
2627
import graphql.schema.TypeResolver;
@@ -101,6 +102,11 @@ public GraphQlSetup typeResolver(TypeResolver typeResolver) {
101102
return this;
102103
}
103104

105+
public GraphQlSetup preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) {
106+
this.graphQlSourceBuilder.preparsedDocumentProvider(preparsedDocumentProvider);
107+
return this;
108+
}
109+
104110
public GraphQlSetup typeVisitor(GraphQLTypeVisitor... visitors) {
105111
this.graphQlSourceBuilder.typeVisitors(Arrays.asList(visitors));
106112
return this;

0 commit comments

Comments
 (0)