diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index fa2ffe21ad..f2da73f034 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -3,6 +3,7 @@ import graphql.collect.ImmutableKit; import graphql.execution.ExecutionId; import graphql.execution.RawVariables; +import graphql.execution.preparsed.persisted.PersistedQuerySupport; import org.dataloader.DataLoaderRegistry; import java.util.Locale; @@ -31,10 +32,19 @@ public class ExecutionInput { private final Locale locale; private final AtomicBoolean cancelled; + /** + * In order for {@link #getQuery()} to never be null, use this to mark + * them so that invariant can be satisfied while assuming that the persisted query + * id is elsewhere. + */ + public final static String PERSISTED_QUERY_MARKER = PersistedQuerySupport.PERSISTED_QUERY_MARKER; + + private final static String APOLLO_AUTOMATIC_PERSISTED_QUERY_EXTENSION = "persistedQuery"; + @Internal private ExecutionInput(Builder builder) { - this.query = assertNotNull(builder.query, () -> "query can't be null"); + this.query = assertQuery(builder); this.operationName = builder.operationName; this.context = builder.context; this.graphQLContext = assertNotNull(builder.graphQLContext); @@ -48,6 +58,30 @@ private ExecutionInput(Builder builder) { this.cancelled = builder.cancelled; } + private static String assertQuery(Builder builder) { + if ((builder.query == null || builder.query.isEmpty()) && isPersistedQuery(builder)) { + return PERSISTED_QUERY_MARKER; + } + + return assertNotNull(builder.query, () -> "query can't be null"); + } + + /** + * This is used to determine if this execution input is a persisted query or not. + * + * @implNote The current implementation supports Apollo Persisted Queries (APQ) by checking for + * the extensions property for the persisted query extension. + * See Apollo Persisted Queries for more details. + * + * @param builder the builder to check + * + * @return true if this is a persisted query + */ + private static boolean isPersistedQuery(Builder builder) { + return builder.extensions != null && + builder.extensions.containsKey(APOLLO_AUTOMATIC_PERSISTED_QUERY_EXTENSION); + } + /** * @return the query text */ @@ -252,7 +286,7 @@ GraphQLContext graphQLContext() { } public Builder query(String query) { - this.query = assertNotNull(query, () -> "query can't be null"); + this.query = query; return this; } @@ -312,7 +346,7 @@ public Builder context(Object context) { return this; } - /** + /** * This will give you a builder of {@link GraphQLContext} and any values you set will be copied * into the underlying {@link GraphQLContext} of this execution input * @@ -393,4 +427,4 @@ public ExecutionInput build() { return new ExecutionInput(this); } } -} +} \ No newline at end of file diff --git a/src/test/groovy/graphql/ExecutionInputTest.groovy b/src/test/groovy/graphql/ExecutionInputTest.groovy index 54ff68d735..23c06ea9c7 100644 --- a/src/test/groovy/graphql/ExecutionInputTest.groovy +++ b/src/test/groovy/graphql/ExecutionInputTest.groovy @@ -8,6 +8,7 @@ import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters +import graphql.execution.preparsed.persisted.PersistedQuerySupport import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import org.dataloader.DataLoaderRegistry @@ -45,6 +46,21 @@ class ExecutionInputTest extends Specification { executionInput.query == query executionInput.locale == Locale.GERMAN executionInput.extensions == [some: "map"] + executionInput.toString() != null + } + + def "build without locale"() { + when: + def executionInput = ExecutionInput.newExecutionInput().query(query) + .dataLoaderRegistry(registry) + .variables(variables) + .root(root) + .graphQLContext({ it.of(["a": "b"]) }) + .locale(null) + .extensions([some: "map"]) + .build() + then: + executionInput.locale == Locale.getDefault() } def "map context build works"() { @@ -314,6 +330,33 @@ class ExecutionInputTest extends Specification { "1000 ms" | plusOrMinus(1000) } + def "uses persisted query marker when query is null"() { + when: + ExecutionInput.newExecutionInput().query(null).build() + then: + thrown(AssertException) + } + + def "uses persisted query marker when query is null and extensions contains persistedQuery"() { + when: + def executionInput = ExecutionInput.newExecutionInput() + .extensions([persistedQuery: "any"]) + .query(null) + .build() + then: + executionInput.query == PersistedQuerySupport.PERSISTED_QUERY_MARKER + } + + def "uses persisted query marker when query is empty and extensions contains persistedQuery"() { + when: + def executionInput = ExecutionInput.newExecutionInput() + .extensions([persistedQuery: "any"]) + .query("") + .build() + then: + executionInput.query == PersistedQuerySupport.PERSISTED_QUERY_MARKER + } + def "can cancel at specific places"() { def sdl = ''' type Query {