From ae55a750f5ef2f24db15c96298e245211e20d53b Mon Sep 17 00:00:00 2001 From: Jordie Date: Wed, 22 Dec 2021 14:27:42 +0100 Subject: [PATCH 1/4] Add PreparsedDocumentProvider support --- .../src/docs/asciidoc/index.adoc | 36 +++++++++ .../DefaultGraphQlSourceBuilder.java | 40 +++++++--- .../graphql/execution/GraphQlSource.java | 19 +++++ .../SpringNoOpPreparsedDocumentProvider.java | 25 ++++++ .../PreparsedDocumentProviderTests.java | 79 +++++++++++++++++++ ...ingNoOpPreparsedDocumentProviderTests.java | 34 ++++++++ .../springframework/graphql/GraphQlSetup.java | 6 ++ 7 files changed, 227 insertions(+), 12 deletions(-) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 4f1c89b57..8c3f91b94 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -231,6 +231,42 @@ name mappings that should help to cover more corner cases. +[[execution-graphqlsource-preparsed-document-provider]] +==== PreparsedDocumentProvider + +You can configure a `PreparsedDocumentProvider` in `GraphQlSource.Builder` to implement `Document` caching and checks. +Doing so makes it possible to skip the parsing and validation steps of execution entirely. It may also assist you +in implementing query whitelisting. + +A simple caching implementation using Caffeine would look something like the following: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +public class CachingPreparsedDocumentProvider implements PreparsedDocumentProvider { + + private final Cache cache = Caffeine + .newBuilder() + .maximumSize(2500) + .build(); + + @Override + public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, + Function parseAndValidateFunction) { + return cache.get(executionInput.getQuery(), queryKey -> parseAndValidateFunction.apply(executionInput)); + } + +} +---- + +Please note that caching in the preceding snippet only works when you parameterize your operation using variables: +[source,graphql,indent=0,subs="verbatim,quotes"] +---- +query HelloTo($to: String!) { + sayHello(to: $to) { + greeting + } +} +---- [[execution-reactive-datafetcher]] === Reactive `DataFetcher` diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java index 2d492754b..1be8a419e 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java @@ -16,19 +16,10 @@ package org.springframework.graphql.execution; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Consumer; - import graphql.GraphQL; import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.language.InterfaceTypeDefinition; import graphql.language.UnionTypeDefinition; import graphql.schema.GraphQLCodeRegistry; @@ -40,11 +31,21 @@ import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; - import org.springframework.core.io.Resource; +import org.springframework.graphql.execution.preparsed.SpringNoOpPreparsedDocumentProvider; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; + /** * Default implementation of {@link GraphQlSource.Builder} that initializes a * {@link GraphQL} instance and wraps it with a {@link GraphQlSource} that returns it. @@ -67,12 +68,15 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { private final List instrumentations = new ArrayList<>(); + @Nullable + private PreparsedDocumentProvider preparsedDocumentProvider; + @Nullable private BiFunction schemaFactory; private Consumer graphQlConfigurers = (builder) -> { }; - + @Override public GraphQlSource.Builder schemaResources(Resource... resources) { @@ -92,6 +96,12 @@ public GraphQlSource.Builder defaultTypeResolver(TypeResolver typeResolver) { return this; } + @Override + public GraphQlSource.Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { + this.preparsedDocumentProvider = preparsedDocumentProvider; + return this; + } + @Override public GraphQlSource.Builder exceptionResolvers(List resolvers) { this.exceptionResolvers.addAll(resolvers); @@ -148,6 +158,12 @@ public GraphQlSource build() { builder = builder.instrumentation(new ChainedInstrumentation(this.instrumentations)); } + PreparsedDocumentProvider preparsedDocumentProvider = (this.preparsedDocumentProvider != null ? + this.preparsedDocumentProvider : + SpringNoOpPreparsedDocumentProvider.INSTANCE); + + builder = builder.preparsedDocumentProvider(preparsedDocumentProvider); + this.graphQlConfigurers.accept(builder); GraphQL graphQl = builder.build(); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java index 29bbcf770..bee1902f1 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java @@ -23,6 +23,8 @@ import graphql.GraphQL; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.preparsed.PreparsedDocumentProvider; +import graphql.language.Document; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLTypeVisitor; import graphql.schema.TypeResolver; @@ -30,6 +32,7 @@ import graphql.schema.idl.TypeDefinitionRegistry; import org.springframework.core.io.Resource; +import org.springframework.graphql.execution.preparsed.SpringNoOpPreparsedDocumentProvider; /** * Strategy to resolve the {@link GraphQL} instance to use. @@ -109,6 +112,22 @@ interface Builder { */ Builder defaultTypeResolver(TypeResolver typeResolver); + /** + * Configure the {@link PreparsedDocumentProvider} to use for GraphQL requests. + *

+ * A {@code PreparsedDocumentProvider} can be used to cache and/or whitelist + * {@link Document} instances for queries. Configuring a + * {@code PreparsedDocumentProvider} gives you the ability to skip query parsing + * and validation. + *

+ * By default, this is set to {@link SpringNoOpPreparsedDocumentProvider}, which + * calls the {@code parseAndValidateFunction}, and does nothing else. + * @param preparsedDocumentProvider the {@code PreparsedDocumentProvider} to use + * @return the current builder + * @see GraphQL#getPreparsedDocumentProvider() + */ + Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider); + /** * Add {@link DataFetcherExceptionResolver}'s to use for resolving exceptions from * {@link graphql.schema.DataFetcher}'s. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java new file mode 100644 index 000000000..a8cf16cdb --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java @@ -0,0 +1,25 @@ +package org.springframework.graphql.execution.preparsed; + +import graphql.ExecutionInput; +import graphql.execution.preparsed.PreparsedDocumentEntry; +import graphql.execution.preparsed.PreparsedDocumentProvider; + +import java.util.function.Function; + +/** + * A {@link PreparsedDocumentProvider} calling the {@code parseAndValidateFunction}, and doing nothing else. + */ +public final class SpringNoOpPreparsedDocumentProvider implements PreparsedDocumentProvider { + + public static final SpringNoOpPreparsedDocumentProvider INSTANCE = new SpringNoOpPreparsedDocumentProvider(); + + private SpringNoOpPreparsedDocumentProvider() { + } + + @Override + public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, + Function parseAndValidateFunction) { + return parseAndValidateFunction.apply(executionInput); + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java new file mode 100644 index 000000000..98a1ac397 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java @@ -0,0 +1,79 @@ +package org.springframework.graphql.execution.preparsed; + +import graphql.ExecutionInput; +import graphql.GraphQL; +import graphql.execution.preparsed.PreparsedDocumentEntry; +import graphql.execution.preparsed.PreparsedDocumentProvider; +import org.junit.jupiter.api.Test; +import org.springframework.graphql.GraphQlSetup; +import org.springframework.graphql.execution.GraphQlSource; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for + * {@link GraphQlSource.Builder#preparsedDocumentProvider(PreparsedDocumentProvider)}. + */ +public class PreparsedDocumentProviderTests { + + private static final String recursiveTestSchema = "type Query { query: Query! }"; + + private static class CountingPreparsedDocumentProvider implements PreparsedDocumentProvider { + + // + private final ConcurrentMap counter; + + private CountingPreparsedDocumentProvider() { + this.counter = new ConcurrentHashMap<>(); + } + + @Override + public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, + Function parseAndValidateFunction) { + counter.compute(executionInput.getQuery(), (k, v) -> v == null ? 1 : v + 1); + return parseAndValidateFunction.apply(executionInput); + } + + public int getCount(String query) { + return counter.getOrDefault(query, 0); + } + + } + + @Test + public void correctDocumentProviderIsSet() { + PreparsedDocumentProvider sample = new CountingPreparsedDocumentProvider(); + GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).preparsedDocumentProvider(sample).toGraphQl(); + assertThat(graphQL.getPreparsedDocumentProvider()).isEqualTo(sample); + } + + @Test + public void noOpProviderIsSetByDefault() { + GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).toGraphQl(); + assertThat(graphQL.getPreparsedDocumentProvider()).isInstanceOf(SpringNoOpPreparsedDocumentProvider.class); + } + + @Test + public void preparsedDocumentProviderWorks() { + CountingPreparsedDocumentProvider sample = new CountingPreparsedDocumentProvider(); + GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).preparsedDocumentProvider(sample).toGraphQl(); + graphQL.execute("{query }"); + graphQL.execute("{query }"); + graphQL.execute("{query}"); + graphQL.execute("{query }"); + graphQL.execute("{query }"); + graphQL.execute("{query }"); + graphQL.execute("{query }"); + graphQL.execute("{query}"); + graphQL.execute("{query }"); + assertThat(sample.getCount("{query}")).isEqualTo(2); + assertThat(sample.getCount("{query }")).isEqualTo(3); + assertThat(sample.getCount("{query }")).isEqualTo(4); + assertThat(sample.getCount("{ query }")).isEqualTo(0); + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java new file mode 100644 index 000000000..0ed2f51b1 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java @@ -0,0 +1,34 @@ +package org.springframework.graphql.execution.preparsed; + +import graphql.ExecutionInput; +import graphql.execution.preparsed.PreparsedDocumentEntry; +import graphql.execution.preparsed.PreparsedDocumentProvider; +import graphql.language.Document; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringNoOpPreparsedDocumentProvider}. + */ +public class SpringNoOpPreparsedDocumentProviderTests { + + @Test + public void noOpDocumentProviderAppliesFunction() { + SpringNoOpPreparsedDocumentProvider documentProvider = SpringNoOpPreparsedDocumentProvider.INSTANCE; + PreparsedDocumentEntry documentEntry = new PreparsedDocumentEntry(Document.newDocument().build()); + PreparsedDocumentEntry providerResult = documentProvider + .getDocument(ExecutionInput.newExecutionInput("{}").build(), executionInput -> documentEntry); + + assertThat(documentEntry).isEqualTo(providerResult); + } + + @Test + public void springNoOpDocumentProviderInstanceIsNotNull() { + assertThat(SpringNoOpPreparsedDocumentProvider.INSTANCE).isNotNull(); + } + +} diff --git a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java index 8402428c6..30427b442 100644 --- a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java +++ b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java @@ -21,6 +21,7 @@ import java.util.List; import graphql.GraphQL; +import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.schema.DataFetcher; import graphql.schema.GraphQLTypeVisitor; import graphql.schema.TypeResolver; @@ -101,6 +102,11 @@ public GraphQlSetup typeResolver(TypeResolver typeResolver) { return this; } + public GraphQlSetup preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { + this.graphQlSourceBuilder.preparsedDocumentProvider(preparsedDocumentProvider); + return this; + } + public GraphQlSetup typeVisitor(GraphQLTypeVisitor... visitors) { this.graphQlSourceBuilder.typeVisitors(Arrays.asList(visitors)); return this; From 05b95b01b93562ba0f4998a05100e521872ba8a6 Mon Sep 17 00:00:00 2001 From: Jordie Date: Sun, 30 Jan 2022 13:06:19 +0100 Subject: [PATCH 2/4] Revert PreparsedDocumentProvider in GraphQlSource --- .../DefaultGraphQlSourceBuilder.java | 17 ---- .../graphql/execution/GraphQlSource.java | 19 ----- .../SpringNoOpPreparsedDocumentProvider.java | 25 ------ .../PreparsedDocumentProviderTests.java | 79 ------------------- ...ingNoOpPreparsedDocumentProviderTests.java | 34 -------- .../springframework/graphql/GraphQlSetup.java | 6 -- 6 files changed, 180 deletions(-) delete mode 100644 spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java delete mode 100644 spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java delete mode 100644 spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java index e45e62957..a5de8bb9f 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java @@ -29,7 +29,6 @@ import graphql.GraphQL; import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.language.InterfaceTypeDefinition; import graphql.language.UnionTypeDefinition; import graphql.schema.GraphQLCodeRegistry; @@ -46,7 +45,6 @@ import graphql.schema.idl.WiringFactory; import org.springframework.core.io.Resource; -import org.springframework.graphql.execution.preparsed.SpringNoOpPreparsedDocumentProvider; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -72,9 +70,6 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { private final List instrumentations = new ArrayList<>(); - @Nullable - private PreparsedDocumentProvider preparsedDocumentProvider; - @Nullable private BiFunction schemaFactory; @@ -99,12 +94,6 @@ public GraphQlSource.Builder defaultTypeResolver(TypeResolver typeResolver) { return this; } - @Override - public GraphQlSource.Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { - this.preparsedDocumentProvider = preparsedDocumentProvider; - return this; - } - @Override public GraphQlSource.Builder exceptionResolvers(List resolvers) { this.exceptionResolvers.addAll(resolvers); @@ -159,12 +148,6 @@ public GraphQlSource build() { builder = builder.instrumentation(new ChainedInstrumentation(this.instrumentations)); } - PreparsedDocumentProvider preparsedDocumentProvider = (this.preparsedDocumentProvider != null ? - this.preparsedDocumentProvider : - SpringNoOpPreparsedDocumentProvider.INSTANCE); - - builder = builder.preparsedDocumentProvider(preparsedDocumentProvider); - this.graphQlConfigurers.accept(builder); GraphQL graphQl = builder.build(); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java index bee1902f1..29bbcf770 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java @@ -23,8 +23,6 @@ import graphql.GraphQL; import graphql.execution.instrumentation.Instrumentation; -import graphql.execution.preparsed.PreparsedDocumentProvider; -import graphql.language.Document; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLTypeVisitor; import graphql.schema.TypeResolver; @@ -32,7 +30,6 @@ import graphql.schema.idl.TypeDefinitionRegistry; import org.springframework.core.io.Resource; -import org.springframework.graphql.execution.preparsed.SpringNoOpPreparsedDocumentProvider; /** * Strategy to resolve the {@link GraphQL} instance to use. @@ -112,22 +109,6 @@ interface Builder { */ Builder defaultTypeResolver(TypeResolver typeResolver); - /** - * Configure the {@link PreparsedDocumentProvider} to use for GraphQL requests. - *

- * A {@code PreparsedDocumentProvider} can be used to cache and/or whitelist - * {@link Document} instances for queries. Configuring a - * {@code PreparsedDocumentProvider} gives you the ability to skip query parsing - * and validation. - *

- * By default, this is set to {@link SpringNoOpPreparsedDocumentProvider}, which - * calls the {@code parseAndValidateFunction}, and does nothing else. - * @param preparsedDocumentProvider the {@code PreparsedDocumentProvider} to use - * @return the current builder - * @see GraphQL#getPreparsedDocumentProvider() - */ - Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider); - /** * Add {@link DataFetcherExceptionResolver}'s to use for resolving exceptions from * {@link graphql.schema.DataFetcher}'s. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java deleted file mode 100644 index a8cf16cdb..000000000 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.springframework.graphql.execution.preparsed; - -import graphql.ExecutionInput; -import graphql.execution.preparsed.PreparsedDocumentEntry; -import graphql.execution.preparsed.PreparsedDocumentProvider; - -import java.util.function.Function; - -/** - * A {@link PreparsedDocumentProvider} calling the {@code parseAndValidateFunction}, and doing nothing else. - */ -public final class SpringNoOpPreparsedDocumentProvider implements PreparsedDocumentProvider { - - public static final SpringNoOpPreparsedDocumentProvider INSTANCE = new SpringNoOpPreparsedDocumentProvider(); - - private SpringNoOpPreparsedDocumentProvider() { - } - - @Override - public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, - Function parseAndValidateFunction) { - return parseAndValidateFunction.apply(executionInput); - } - -} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java deleted file mode 100644 index 98a1ac397..000000000 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/PreparsedDocumentProviderTests.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.springframework.graphql.execution.preparsed; - -import graphql.ExecutionInput; -import graphql.GraphQL; -import graphql.execution.preparsed.PreparsedDocumentEntry; -import graphql.execution.preparsed.PreparsedDocumentProvider; -import org.junit.jupiter.api.Test; -import org.springframework.graphql.GraphQlSetup; -import org.springframework.graphql.execution.GraphQlSource; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for - * {@link GraphQlSource.Builder#preparsedDocumentProvider(PreparsedDocumentProvider)}. - */ -public class PreparsedDocumentProviderTests { - - private static final String recursiveTestSchema = "type Query { query: Query! }"; - - private static class CountingPreparsedDocumentProvider implements PreparsedDocumentProvider { - - // - private final ConcurrentMap counter; - - private CountingPreparsedDocumentProvider() { - this.counter = new ConcurrentHashMap<>(); - } - - @Override - public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, - Function parseAndValidateFunction) { - counter.compute(executionInput.getQuery(), (k, v) -> v == null ? 1 : v + 1); - return parseAndValidateFunction.apply(executionInput); - } - - public int getCount(String query) { - return counter.getOrDefault(query, 0); - } - - } - - @Test - public void correctDocumentProviderIsSet() { - PreparsedDocumentProvider sample = new CountingPreparsedDocumentProvider(); - GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).preparsedDocumentProvider(sample).toGraphQl(); - assertThat(graphQL.getPreparsedDocumentProvider()).isEqualTo(sample); - } - - @Test - public void noOpProviderIsSetByDefault() { - GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).toGraphQl(); - assertThat(graphQL.getPreparsedDocumentProvider()).isInstanceOf(SpringNoOpPreparsedDocumentProvider.class); - } - - @Test - public void preparsedDocumentProviderWorks() { - CountingPreparsedDocumentProvider sample = new CountingPreparsedDocumentProvider(); - GraphQL graphQL = GraphQlSetup.schemaContent(recursiveTestSchema).preparsedDocumentProvider(sample).toGraphQl(); - graphQL.execute("{query }"); - graphQL.execute("{query }"); - graphQL.execute("{query}"); - graphQL.execute("{query }"); - graphQL.execute("{query }"); - graphQL.execute("{query }"); - graphQL.execute("{query }"); - graphQL.execute("{query}"); - graphQL.execute("{query }"); - assertThat(sample.getCount("{query}")).isEqualTo(2); - assertThat(sample.getCount("{query }")).isEqualTo(3); - assertThat(sample.getCount("{query }")).isEqualTo(4); - assertThat(sample.getCount("{ query }")).isEqualTo(0); - } - -} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java deleted file mode 100644 index 0ed2f51b1..000000000 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/preparsed/SpringNoOpPreparsedDocumentProviderTests.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.springframework.graphql.execution.preparsed; - -import graphql.ExecutionInput; -import graphql.execution.preparsed.PreparsedDocumentEntry; -import graphql.execution.preparsed.PreparsedDocumentProvider; -import graphql.language.Document; -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link SpringNoOpPreparsedDocumentProvider}. - */ -public class SpringNoOpPreparsedDocumentProviderTests { - - @Test - public void noOpDocumentProviderAppliesFunction() { - SpringNoOpPreparsedDocumentProvider documentProvider = SpringNoOpPreparsedDocumentProvider.INSTANCE; - PreparsedDocumentEntry documentEntry = new PreparsedDocumentEntry(Document.newDocument().build()); - PreparsedDocumentEntry providerResult = documentProvider - .getDocument(ExecutionInput.newExecutionInput("{}").build(), executionInput -> documentEntry); - - assertThat(documentEntry).isEqualTo(providerResult); - } - - @Test - public void springNoOpDocumentProviderInstanceIsNotNull() { - assertThat(SpringNoOpPreparsedDocumentProvider.INSTANCE).isNotNull(); - } - -} diff --git a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java index 30427b442..8402428c6 100644 --- a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java +++ b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java @@ -21,7 +21,6 @@ import java.util.List; import graphql.GraphQL; -import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.schema.DataFetcher; import graphql.schema.GraphQLTypeVisitor; import graphql.schema.TypeResolver; @@ -102,11 +101,6 @@ public GraphQlSetup typeResolver(TypeResolver typeResolver) { return this; } - public GraphQlSetup preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { - this.graphQlSourceBuilder.preparsedDocumentProvider(preparsedDocumentProvider); - return this; - } - public GraphQlSetup typeVisitor(GraphQLTypeVisitor... visitors) { this.graphQlSourceBuilder.typeVisitors(Arrays.asList(visitors)); return this; From 9f9d0938269da4864c0abbe4a3aafa62607a87cb Mon Sep 17 00:00:00 2001 From: Jordie Date: Sun, 30 Jan 2022 13:06:35 +0100 Subject: [PATCH 3/4] Modify PreparsedDocumentProvider documentation --- spring-graphql-docs/src/docs/asciidoc/index.adoc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 7124f6547..5004b9bb9 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -288,11 +288,15 @@ name mappings that should help to cover more corner cases. [[execution-graphqlsource-preparsed-document-provider]] ==== PreparsedDocumentProvider -You can configure a `PreparsedDocumentProvider` in `GraphQlSource.Builder` to implement `Document` caching and checks. -Doing so makes it possible to skip the parsing and validation steps of execution entirely. It may also assist you -in implementing query whitelisting. +Before operations can be executed by GraphQL Java, their request string must be _parsed_ and _validated_. These +two steps may impact the performance of applications significantly. -A simple caching implementation using Caffeine would look something like the following: +You may configure a `PreparsedDocumentProvider` using `GraphQlSource.Builder#configureGraphQl`. The +`PreparsedDocumentProvider` can intercept these two steps and gives library consumers the tools to +cache, or modify the resulting operation. + +The following snippet uses https://github.com/ben-manes/caffeine[Caffeine] to build a `PreparsedDocumentProvider` +which caches the 2500 most recent operations for a maximum of 1 hour: [source,java,indent=0,subs="verbatim,quotes"] ---- From d047d3cbedb8e8d60bc678605000515966b40c7a Mon Sep 17 00:00:00 2001 From: Jordie Date: Sun, 30 Jan 2022 13:12:13 +0100 Subject: [PATCH 4/4] queryKey -> operationKey --- spring-graphql-docs/src/docs/asciidoc/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 5004b9bb9..45acd903f 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -310,7 +310,7 @@ public class CachingPreparsedDocumentProvider implements PreparsedDocumentProvid @Override public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction) { - return cache.get(executionInput.getQuery(), queryKey -> parseAndValidateFunction.apply(executionInput)); + return cache.get(executionInput.getQuery(), operationKey -> parseAndValidateFunction.apply(executionInput)); } }