Skip to content

Commit 16b0262

Browse files
committed
Add normalized document provider to allow caching of normalized documents
Fix
1 parent 1b8640a commit 16b0262

File tree

10 files changed

+163
-29
lines changed

10 files changed

+163
-29
lines changed

src/main/java/graphql/GraphQL.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import graphql.execution.ExecutionId;
1010
import graphql.execution.ExecutionIdProvider;
1111
import graphql.execution.ExecutionStrategy;
12-
import graphql.execution.ResponseMapFactory;
1312
import graphql.execution.SimpleDataFetcherExceptionHandler;
1413
import graphql.execution.SubscriptionExecutionStrategy;
1514
import graphql.execution.ValueUnboxer;
@@ -25,6 +24,8 @@
2524
import graphql.execution.preparsed.PreparsedDocumentEntry;
2625
import graphql.execution.preparsed.PreparsedDocumentProvider;
2726
import graphql.language.Document;
27+
import graphql.normalized.nf.provider.NoOpNormalizedDocumentProvider;
28+
import graphql.normalized.nf.provider.NormalizedDocumentProvider;
2829
import graphql.schema.GraphQLSchema;
2930
import graphql.validation.ValidationError;
3031

@@ -158,6 +159,7 @@ public static GraphQLUnusualConfiguration.GraphQLContextConfiguration unusualCon
158159
private final ExecutionIdProvider idProvider;
159160
private final Instrumentation instrumentation;
160161
private final PreparsedDocumentProvider preparsedDocumentProvider;
162+
private final NormalizedDocumentProvider normalizedDocumentProvider;
161163
private final ValueUnboxer valueUnboxer;
162164
private final boolean doNotAutomaticallyDispatchDataLoader;
163165

@@ -170,6 +172,7 @@ private GraphQL(Builder builder) {
170172
this.idProvider = assertNotNull(builder.idProvider, () -> "idProvider must be non null");
171173
this.instrumentation = assertNotNull(builder.instrumentation, () -> "instrumentation must not be null");
172174
this.preparsedDocumentProvider = assertNotNull(builder.preparsedDocumentProvider, () -> "preparsedDocumentProvider must be non null");
175+
this.normalizedDocumentProvider = assertNotNull(builder.normalizedDocumentProvider, () -> "normalizedDocumentProvider must be non null");
173176
this.valueUnboxer = assertNotNull(builder.valueUnboxer, () -> "valueUnboxer must not be null");
174177
this.doNotAutomaticallyDispatchDataLoader = builder.doNotAutomaticallyDispatchDataLoader;
175178
}
@@ -227,6 +230,13 @@ public PreparsedDocumentProvider getPreparsedDocumentProvider() {
227230
return preparsedDocumentProvider;
228231
}
229232

233+
/**
234+
* @return the NormalizedDocumentProvider for this {@link GraphQL} instance
235+
*/
236+
public NormalizedDocumentProvider getNormalizedDocumentProvider() {
237+
return normalizedDocumentProvider;
238+
}
239+
230240
/**
231241
* @return the ValueUnboxer for this {@link GraphQL} instance
232242
*/
@@ -261,7 +271,8 @@ public GraphQL transform(Consumer<GraphQL.Builder> builderConsumer) {
261271
.subscriptionExecutionStrategy(this.subscriptionStrategy)
262272
.executionIdProvider(Optional.ofNullable(this.idProvider).orElse(builder.idProvider))
263273
.instrumentation(Optional.ofNullable(this.instrumentation).orElse(builder.instrumentation))
264-
.preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider));
274+
.preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider))
275+
.normalizedDocumentProvider(Optional.ofNullable(this.normalizedDocumentProvider).orElse(builder.normalizedDocumentProvider));
265276

266277
builderConsumer.accept(builder);
267278

@@ -278,6 +289,7 @@ public static class Builder {
278289
private ExecutionIdProvider idProvider = DEFAULT_EXECUTION_ID_PROVIDER;
279290
private Instrumentation instrumentation = null; // deliberate default here
280291
private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE;
292+
private NormalizedDocumentProvider normalizedDocumentProvider = NoOpNormalizedDocumentProvider.INSTANCE;
281293
private boolean doNotAutomaticallyDispatchDataLoader = false;
282294
private ValueUnboxer valueUnboxer = ValueUnboxer.DEFAULT;
283295

@@ -328,6 +340,18 @@ public Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocu
328340
return this;
329341
}
330342

343+
/**
344+
* This allows you to set a {@link NormalizedDocumentProvider} that will be used to provide normalized documents
345+
*
346+
* @param normalizedDocumentProvider the provider of normalized documents
347+
*
348+
* @return this builder
349+
*/
350+
public Builder normalizedDocumentProvider(NormalizedDocumentProvider normalizedDocumentProvider) {
351+
this.normalizedDocumentProvider = normalizedDocumentProvider;
352+
return this;
353+
}
354+
331355
public Builder executionIdProvider(ExecutionIdProvider executionIdProvider) {
332356
this.idProvider = assertNotNull(executionIdProvider, () -> "ExecutionIdProvider must be non null");
333357
return this;
@@ -497,7 +521,7 @@ public CompletableFuture<ExecutionResult> executeAsync(ExecutionInput executionI
497521

498522
GraphQLSchema graphQLSchema = instrumentation.instrumentSchema(this.graphQLSchema, instrumentationParameters, instrumentationState);
499523

500-
CompletableFuture<ExecutionResult> executionResult = parseValidateAndExecute(instrumentedExecutionInput, graphQLSchema, instrumentationState, engineRunningState);
524+
CompletableFuture<ExecutionResult> executionResult = parseValidateAndExecute(instrumentedExecutionInput, graphQLSchema, instrumentationState, engineRunningState, normalizedDocumentProvider);
501525
//
502526
// finish up instrumentation
503527
executionResult = executionResult.whenComplete(completeInstrumentationCtxCF(executionInstrumentation));
@@ -529,7 +553,7 @@ private ExecutionInput ensureInputHasId(ExecutionInput executionInput) {
529553
}
530554

531555

532-
private CompletableFuture<ExecutionResult> parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, EngineRunningState engineRunningState) {
556+
private CompletableFuture<ExecutionResult> parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState, EngineRunningState engineRunningState, NormalizedDocumentProvider normalizedDocumentProvider) {
533557
AtomicReference<ExecutionInput> executionInputRef = new AtomicReference<>(executionInput);
534558
Function<ExecutionInput, PreparsedDocumentEntry> computeFunction = transformedInput -> {
535559
// if they change the original query in the pre-parser, then we want to see it downstream from then on
@@ -542,7 +566,7 @@ private CompletableFuture<ExecutionResult> parseValidateAndExecute(ExecutionInpu
542566
return CompletableFuture.completedFuture(new ExecutionResultImpl(preparsedDocumentEntry.getErrors()));
543567
}
544568
try {
545-
return execute(executionInputRef.get(), preparsedDocumentEntry.getDocument(), graphQLSchema, instrumentationState, engineRunningState);
569+
return execute(executionInputRef.get(), preparsedDocumentEntry.getDocument(), graphQLSchema, instrumentationState, engineRunningState, normalizedDocumentProvider);
546570
} catch (AbortExecutionException e) {
547571
return CompletableFuture.completedFuture(e.toExecutionResult());
548572
}
@@ -606,10 +630,11 @@ private CompletableFuture<ExecutionResult> execute(ExecutionInput executionInput
606630
Document document,
607631
GraphQLSchema graphQLSchema,
608632
InstrumentationState instrumentationState,
609-
EngineRunningState engineRunningState
633+
EngineRunningState engineRunningState,
634+
NormalizedDocumentProvider normalizedDocumentProvider
610635
) {
611636

612-
Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, doNotAutomaticallyDispatchDataLoader);
637+
Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, doNotAutomaticallyDispatchDataLoader, normalizedDocumentProvider);
613638
ExecutionId executionId = executionInput.getExecutionId();
614639

615640
return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState, engineRunningState);

src/main/java/graphql/execution/Execution.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import graphql.language.NodeUtil;
2626
import graphql.language.OperationDefinition;
2727
import graphql.language.VariableDefinition;
28+
import graphql.normalized.nf.provider.NormalizedDocumentProvider;
2829
import graphql.schema.GraphQLObjectType;
2930
import graphql.schema.GraphQLSchema;
3031
import graphql.schema.impl.SchemaUtil;
@@ -55,20 +56,22 @@ public class Execution {
5556
private final Instrumentation instrumentation;
5657
private final ValueUnboxer valueUnboxer;
5758
private final boolean doNotAutomaticallyDispatchDataLoader;
58-
59+
private final NormalizedDocumentProvider normalizedDocumentProvider;
5960

6061
public Execution(ExecutionStrategy queryStrategy,
6162
ExecutionStrategy mutationStrategy,
6263
ExecutionStrategy subscriptionStrategy,
6364
Instrumentation instrumentation,
6465
ValueUnboxer valueUnboxer,
65-
boolean doNotAutomaticallyDispatchDataLoader) {
66+
boolean doNotAutomaticallyDispatchDataLoader,
67+
NormalizedDocumentProvider normalizedDocumentProvider) {
6668
this.queryStrategy = queryStrategy != null ? queryStrategy : new AsyncExecutionStrategy();
6769
this.mutationStrategy = mutationStrategy != null ? mutationStrategy : new AsyncSerialExecutionStrategy();
6870
this.subscriptionStrategy = subscriptionStrategy != null ? subscriptionStrategy : new AsyncExecutionStrategy();
6971
this.instrumentation = instrumentation;
7072
this.valueUnboxer = valueUnboxer;
7173
this.doNotAutomaticallyDispatchDataLoader = doNotAutomaticallyDispatchDataLoader;
74+
this.normalizedDocumentProvider = normalizedDocumentProvider;
7275
}
7376

7477
public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput, InstrumentationState instrumentationState, EngineRunningState engineRunningState) {
@@ -118,6 +121,7 @@ public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSche
118121
.locale(executionInput.getLocale())
119122
.valueUnboxer(valueUnboxer)
120123
.responseMapFactory(responseMapFactory)
124+
.normalizedDocumentProvider(normalizedDocumentProvider)
121125
.executionInput(executionInput)
122126
.propagapropagateErrorsOnNonNullContractFailureeErrors(propagateErrorsOnNonNullContractFailure)
123127
.engineRunningState(engineRunningState)

src/main/java/graphql/execution/ExecutionContext.java

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import graphql.normalized.nf.NormalizedDocument;
2424
import graphql.normalized.nf.NormalizedDocumentFactory;
2525
import graphql.normalized.nf.NormalizedOperation;
26+
import graphql.normalized.nf.provider.NormalizedDocumentProvider;
2627
import graphql.schema.GraphQLSchema;
2728
import graphql.util.FpKit;
2829
import graphql.util.LockKit;
@@ -34,9 +35,11 @@
3435
import java.util.Locale;
3536
import java.util.Map;
3637
import java.util.Set;
38+
import java.util.concurrent.CompletableFuture;
3739
import java.util.concurrent.atomic.AtomicInteger;
3840
import java.util.concurrent.atomic.AtomicReference;
3941
import java.util.function.Consumer;
42+
import java.util.function.Function;
4043
import java.util.function.Supplier;
4144

4245
@SuppressWarnings("TypeParameterUnusedInFormals")
@@ -67,6 +70,7 @@ public class ExecutionContext {
6770
private final IncrementalCallState incrementalCallState = new IncrementalCallState();
6871
private final ValueUnboxer valueUnboxer;
6972
private final ResponseMapFactory responseMapFactory;
73+
private final NormalizedDocumentProvider normalizedDocumentProvider;
7074

7175
private final ExecutionInput executionInput;
7276
private final Supplier<GraphQlNormalizedOperation> queryTree;
@@ -100,11 +104,12 @@ public class ExecutionContext {
100104
this.locale = builder.locale;
101105
this.valueUnboxer = builder.valueUnboxer;
102106
this.responseMapFactory = builder.responseMapFactory;
107+
this.normalizedDocumentProvider = builder.normalizedDocumentProvider;
103108
this.errors.set(builder.errors);
104109
this.localContext = builder.localContext;
105110
this.executionInput = builder.executionInput;
106111
this.dataLoaderDispatcherStrategy = builder.dataLoaderDispatcherStrategy;
107-
this.queryTree = FpKit.interThreadMemoize(this::createGraphQLNormalizedOperation);
112+
this.queryTree = FpKit.interThreadMemoize(() -> this. createGraphQLNormalizedOperation().join());
108113
this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure;
109114
this.engineRunningState = builder.engineRunningState;
110115
}
@@ -307,6 +312,11 @@ public ResponseMapFactory getResponseMapFactory() {
307312
return responseMapFactory;
308313
}
309314

315+
@Internal
316+
public NormalizedDocumentProvider getNormalizedDocumentProvider() {
317+
return normalizedDocumentProvider;
318+
}
319+
310320
/**
311321
* @return the total list of errors for this execution context
312322
*/
@@ -372,29 +382,38 @@ public ResultNodesInfo getResultNodesInfo() {
372382
return resultNodesInfo;
373383
}
374384

375-
private GraphQlNormalizedOperation createGraphQLNormalizedOperation() {
385+
private NormalizedDocument createNormalizedDocument() {
386+
return NormalizedDocumentFactory.createNormalizedDocument(graphQLSchema, document);
387+
}
388+
389+
private CompletableFuture<GraphQlNormalizedOperation> createGraphQLNormalizedOperation() {
376390
// Check for experimental support for normalized documents
377391
if (hasNormalizedDocumentSupport()) {
378-
return createNormalizedOperation();
392+
return createNormalizedOperation()
393+
.thenApply(Function.identity()); // Cast to interface.
379394
}
380395

381-
return createExecutableNormalizedOperation();
396+
return CompletableFuture.completedFuture(createExecutableNormalizedOperation());
382397
}
383398

384399
@ExperimentalApi
385-
private NormalizedOperation createNormalizedOperation() {
386-
var normalizedDocument = NormalizedDocumentFactory.createNormalizedDocument(graphQLSchema, document);
387-
388-
// Search the document for the operation that matches the operationDefinition name,
389-
// if no match then it could be anonymous query, then fallback to the first operation.
390-
var normalizedOperations = normalizedDocument.getNormalizedOperations();
391-
var normalizedOperation = normalizedOperations.stream()
392-
.filter(this::isExecutingOperation)
393-
.findAny()
394-
.map(NormalizedDocument.NormalizedOperationWithAssumedSkipIncludeVariables::getNormalizedOperation)
395-
.orElseGet(normalizedDocument::getSingleNormalizedOperation);
396-
397-
return normalizedOperation;
400+
private CompletableFuture<NormalizedOperation> createNormalizedOperation() {
401+
402+
te return normalizedDocumentProvider.getNormalizedDocument(executionInput, this::createNormalizedDocument).thenApply(normalizedDocumentEntry -> {
403+
var normalizedDocument = normalizedDocumentEntry.getDocument();
404+
405+
// Search the document for the operation that matches the operationDefinition name,
406+
// if no match then it could be anonymous query, then fallback to the first operation.
407+
var normalizedOperations = normalizedDocument.getNormalizedOperations();
408+
var normalizedOperation = normalizedOperations.stream()
409+
.filter(this::isExecutingOperation)
410+
.findAny()
411+
.map(NormalizedDocument.NormalizedOperationWithAssumedSkipIncludeVariables::getNormalizedOperation)
412+
.orElseGet(normalizedDocument::getSingleNormalizedOperation);
413+
414+
415+
return normalizedOperation;
416+
});
398417
}
399418

400419
private ExecutableNormalizedOperation createExecutableNormalizedOperation() {

src/main/java/graphql/execution/ExecutionContextBuilder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import graphql.language.Document;
1515
import graphql.language.FragmentDefinition;
1616
import graphql.language.OperationDefinition;
17+
import graphql.normalized.nf.NormalizedDocumentFactory;
18+
import graphql.normalized.nf.provider.NormalizedDocumentProvider;
1719
import graphql.schema.GraphQLSchema;
1820
import org.dataloader.DataLoaderRegistry;
1921
import org.jspecify.annotations.Nullable;
@@ -53,6 +55,7 @@ public class ExecutionContextBuilder {
5355
boolean propagateErrorsOnNonNullContractFailure = true;
5456
EngineRunningState engineRunningState;
5557
ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT;
58+
NormalizedDocumentProvider normalizedDocumentProvider;
5659

5760
/**
5861
* @return a new builder of {@link graphql.execution.ExecutionContext}s
@@ -102,6 +105,7 @@ public ExecutionContextBuilder() {
102105
propagateErrorsOnNonNullContractFailure = other.propagateErrorsOnNonNullContractFailure();
103106
engineRunningState = other.getEngineRunningState();
104107
responseMapFactory = other.getResponseMapFactory();
108+
normalizedDocumentProvider = other.getNormalizedDocumentProvider();
105109
}
106110

107111
public ExecutionContextBuilder instrumentation(Instrumentation instrumentation) {
@@ -232,6 +236,12 @@ public ExecutionContextBuilder responseMapFactory(ResponseMapFactory responseMap
232236
return this;
233237
}
234238

239+
@Internal
240+
public ExecutionContextBuilder normalizedDocumentProvider(NormalizedDocumentProvider normalizedDocumentProvider) {
241+
this.normalizedDocumentProvider = normalizedDocumentProvider;
242+
return this;
243+
}
244+
235245
public ExecutionContextBuilder resetErrors() {
236246
this.errors = emptyList();
237247
return this;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package graphql.normalized.nf.provider;
2+
3+
import graphql.normalized.nf.NormalizedDocument;
4+
5+
@FunctionalInterface
6+
public interface CreateNormalizedDocument {
7+
NormalizedDocument createNormalizedDocument();
8+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package graphql.normalized.nf.provider;
2+
3+
import graphql.ExecutionInput;
4+
import graphql.Internal;
5+
6+
import java.util.concurrent.CompletableFuture;
7+
8+
@Internal
9+
public class NoOpNormalizedDocumentProvider implements NormalizedDocumentProvider {
10+
public static final NoOpNormalizedDocumentProvider INSTANCE = new NoOpNormalizedDocumentProvider();
11+
12+
@Override
13+
public CompletableFuture<NormalizedDocumentEntry> getNormalizedDocument(ExecutionInput executionInput, CreateNormalizedDocument creator) {
14+
return CompletableFuture.completedFuture(new NormalizedDocumentEntry(creator.createNormalizedDocument()));
15+
}
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package graphql.normalized.nf.provider;
2+
3+
import graphql.GraphQLError;
4+
import graphql.PublicApi;
5+
import graphql.language.Document;
6+
import graphql.normalized.nf.NormalizedDocument;
7+
8+
import java.io.Serializable;
9+
import java.util.List;
10+
11+
import static graphql.Assert.assertNotNull;
12+
import static java.util.Collections.singletonList;
13+
14+
/**
15+
* NOTE: This class implements {@link Serializable} and hence it can be serialised and placed into a distributed cache. However we
16+
* are not aiming to provide long term compatibility and do not intend for you to place this serialised data into permanent storage,
17+
* with times frames that cross graphql-java versions. While we don't change things unnecessarily, we may inadvertently break
18+
* the serialised compatibility across versions.
19+
*/
20+
@PublicApi
21+
public class NormalizedDocumentEntry implements Serializable {
22+
private final NormalizedDocument document;
23+
24+
public NormalizedDocumentEntry(NormalizedDocument document) {
25+
assertNotNull(document);
26+
this.document = document;
27+
}
28+
29+
public NormalizedDocument getDocument() {
30+
return document;
31+
}
32+
}

0 commit comments

Comments
 (0)