Skip to content

Commit 43a54fc

Browse files
authored
Merge pull request #4049 from timward60/jocarrier-apq
Support null query when running APQ request
2 parents b26c7f7 + b460154 commit 43a54fc

File tree

2 files changed

+80
-3
lines changed

2 files changed

+80
-3
lines changed

src/main/java/graphql/ExecutionInput.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import graphql.collect.ImmutableKit;
44
import graphql.execution.ExecutionId;
55
import graphql.execution.RawVariables;
6+
import graphql.execution.preparsed.persisted.PersistedQuerySupport;
67
import org.dataloader.DataLoaderRegistry;
78
import org.jspecify.annotations.NullMarked;
89
import org.jspecify.annotations.NullUnmarked;
@@ -36,10 +37,19 @@ public class ExecutionInput {
3637
private final AtomicBoolean cancelled;
3738
private final boolean profileExecution;
3839

40+
/**
41+
* In order for {@link #getQuery()} to never be null, use this to mark
42+
* them so that invariant can be satisfied while assuming that the persisted query
43+
* id is elsewhere.
44+
*/
45+
public final static String PERSISTED_QUERY_MARKER = PersistedQuerySupport.PERSISTED_QUERY_MARKER;
46+
47+
private final static String APOLLO_AUTOMATIC_PERSISTED_QUERY_EXTENSION = "persistedQuery";
48+
3949

4050
@Internal
4151
private ExecutionInput(Builder builder) {
42-
this.query = assertNotNull(builder.query, () -> "query can't be null");
52+
this.query = assertQuery(builder);
4353
this.operationName = builder.operationName;
4454
this.context = builder.context;
4555
this.graphQLContext = assertNotNull(builder.graphQLContext);
@@ -54,6 +64,30 @@ private ExecutionInput(Builder builder) {
5464
this.profileExecution = builder.profileExecution;
5565
}
5666

67+
private static String assertQuery(Builder builder) {
68+
if ((builder.query == null || builder.query.isEmpty()) && isPersistedQuery(builder)) {
69+
return PERSISTED_QUERY_MARKER;
70+
}
71+
72+
return assertNotNull(builder.query, () -> "query can't be null");
73+
}
74+
75+
/**
76+
* This is used to determine if this execution input is a persisted query or not.
77+
*
78+
* @implNote The current implementation supports Apollo Persisted Queries (APQ) by checking for
79+
* the extensions property for the persisted query extension.
80+
* See <a href="https://www.apollographql.com/docs/apollo-server/performance/apq/">Apollo Persisted Queries</a> for more details.
81+
*
82+
* @param builder the builder to check
83+
*
84+
* @return true if this is a persisted query
85+
*/
86+
private static boolean isPersistedQuery(Builder builder) {
87+
return builder.extensions != null &&
88+
builder.extensions.containsKey(APOLLO_AUTOMATIC_PERSISTED_QUERY_EXTENSION);
89+
}
90+
5791
/**
5892
* @return the query text
5993
*/
@@ -285,7 +319,7 @@ GraphQLContext graphQLContext() {
285319
}
286320

287321
public Builder query(String query) {
288-
this.query = assertNotNull(query, () -> "query can't be null");
322+
this.query = query;
289323
return this;
290324
}
291325

@@ -431,4 +465,4 @@ public ExecutionInput build() {
431465
return new ExecutionInput(this);
432466
}
433467
}
434-
}
468+
}

src/test/groovy/graphql/ExecutionInputTest.groovy

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import graphql.execution.instrumentation.InstrumentationState
88
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters
99
import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters
1010
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters
11+
import graphql.execution.preparsed.persisted.PersistedQuerySupport
1112
import graphql.schema.DataFetcher
1213
import graphql.schema.DataFetchingEnvironment
1314
import org.dataloader.DataLoaderRegistry
@@ -45,6 +46,21 @@ class ExecutionInputTest extends Specification {
4546
executionInput.query == query
4647
executionInput.locale == Locale.GERMAN
4748
executionInput.extensions == [some: "map"]
49+
executionInput.toString() != null
50+
}
51+
52+
def "build without locale"() {
53+
when:
54+
def executionInput = ExecutionInput.newExecutionInput().query(query)
55+
.dataLoaderRegistry(registry)
56+
.variables(variables)
57+
.root(root)
58+
.graphQLContext({ it.of(["a": "b"]) })
59+
.locale(null)
60+
.extensions([some: "map"])
61+
.build()
62+
then:
63+
executionInput.locale == Locale.getDefault()
4864
}
4965

5066
def "map context build works"() {
@@ -314,6 +330,33 @@ class ExecutionInputTest extends Specification {
314330
"1000 ms" | plusOrMinus(1000)
315331
}
316332
333+
def "uses persisted query marker when query is null"() {
334+
when:
335+
ExecutionInput.newExecutionInput().query(null).build()
336+
then:
337+
thrown(AssertException)
338+
}
339+
340+
def "uses persisted query marker when query is null and extensions contains persistedQuery"() {
341+
when:
342+
def executionInput = ExecutionInput.newExecutionInput()
343+
.extensions([persistedQuery: "any"])
344+
.query(null)
345+
.build()
346+
then:
347+
executionInput.query == PersistedQuerySupport.PERSISTED_QUERY_MARKER
348+
}
349+
350+
def "uses persisted query marker when query is empty and extensions contains persistedQuery"() {
351+
when:
352+
def executionInput = ExecutionInput.newExecutionInput()
353+
.extensions([persistedQuery: "any"])
354+
.query("")
355+
.build()
356+
then:
357+
executionInput.query == PersistedQuerySupport.PERSISTED_QUERY_MARKER
358+
}
359+
317360
def "can cancel at specific places"() {
318361
def sdl = '''
319362
type Query {

0 commit comments

Comments
 (0)