Skip to content

Commit fbb6e8b

Browse files
committed
Add normalized document provider to allow caching of normalized documents
Fix
1 parent 21ad7d7 commit fbb6e8b

File tree

10 files changed

+164
-32
lines changed

10 files changed

+164
-32
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: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import graphql.normalized.nf.NormalizedDocument;
2727
import graphql.normalized.nf.NormalizedDocumentFactory;
2828
import graphql.normalized.nf.NormalizedOperation;
29+
import graphql.normalized.nf.provider.NormalizedDocumentProvider;
2930
import graphql.schema.GraphQLSchema;
3031
import graphql.util.FpKit;
3132
import graphql.util.LockKit;
@@ -37,9 +38,11 @@
3738
import java.util.Locale;
3839
import java.util.Map;
3940
import java.util.Set;
41+
import java.util.concurrent.CompletableFuture;
4042
import java.util.concurrent.atomic.AtomicInteger;
4143
import java.util.concurrent.atomic.AtomicReference;
4244
import java.util.function.Consumer;
45+
import java.util.function.Function;
4346
import java.util.function.Supplier;
4447

4548
@SuppressWarnings("TypeParameterUnusedInFormals")
@@ -70,6 +73,7 @@ public class ExecutionContext {
7073
private final IncrementalCallState incrementalCallState = new IncrementalCallState();
7174
private final ValueUnboxer valueUnboxer;
7275
private final ResponseMapFactory responseMapFactory;
76+
private final NormalizedDocumentProvider normalizedDocumentProvider;
7377

7478
private final ExecutionInput executionInput;
7579
private final Supplier<GraphQlNormalizedOperation> queryTree;
@@ -103,11 +107,12 @@ public class ExecutionContext {
103107
this.locale = builder.locale;
104108
this.valueUnboxer = builder.valueUnboxer;
105109
this.responseMapFactory = builder.responseMapFactory;
110+
this.normalizedDocumentProvider = builder.normalizedDocumentProvider;
106111
this.errors.set(builder.errors);
107112
this.localContext = builder.localContext;
108113
this.executionInput = builder.executionInput;
109114
this.dataLoaderDispatcherStrategy = builder.dataLoaderDispatcherStrategy;
110-
this.queryTree = FpKit.interThreadMemoize(this::createGraphQLNormalizedOperation);
115+
this.queryTree = FpKit.interThreadMemoize(() -> this. createGraphQLNormalizedOperation().join());
111116
this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure;
112117
this.engineRunningState = builder.engineRunningState;
113118
}
@@ -310,6 +315,11 @@ public ResponseMapFactory getResponseMapFactory() {
310315
return responseMapFactory;
311316
}
312317

318+
@Internal
319+
public NormalizedDocumentProvider getNormalizedDocumentProvider() {
320+
return normalizedDocumentProvider;
321+
}
322+
313323
/**
314324
* @return the total list of errors for this execution context
315325
*/
@@ -375,35 +385,42 @@ public ResultNodesInfo getResultNodesInfo() {
375385
return resultNodesInfo;
376386
}
377387

378-
private GraphQlNormalizedOperation createGraphQLNormalizedOperation() {
388+
private NormalizedDocument createNormalizedDocument() {
389+
return NormalizedDocumentFactory.createNormalizedDocument(graphQLSchema, document);
390+
}
391+
392+
private CompletableFuture<GraphQlNormalizedOperation> createGraphQLNormalizedOperation() {
379393
// Check for experimental support for normalized documents
380394
if (hasNormalizedDocumentSupport()) {
381-
return createNormalizedOperation();
395+
return createNormalizedOperation()
396+
.thenApply(Function.identity()); // Cast to interface.
382397
}
383398

384-
return createExecutableNormalizedOperation();
399+
return CompletableFuture.completedFuture(createExecutableNormalizedOperation());
385400
}
386401

387402
@ExperimentalApi
388-
private NormalizedOperation createNormalizedOperation() {
403+
private CompletableFuture<NormalizedOperation> createNormalizedOperation() {
389404
var parameters = new InstrumentationCreateNormalizedDocumentParameters(executionInput, graphQLSchema);
390405
var instrument = instrumentation.beginCreateNormalizedDocument(parameters, instrumentationState);
391-
var normalizedDocument = NormalizedDocumentFactory.createNormalizedDocument(graphQLSchema, document);
392-
393-
// Search the document for the operation that matches the operationDefinition name,
394-
// if no match then it could be anonymous query, then fallback to the first operation.
395-
var normalizedOperations = normalizedDocument.getNormalizedOperations();
396-
var normalizedOperation = normalizedOperations.stream()
397-
.filter(this::isExecutingOperation)
398-
.findAny()
399-
.map(NormalizedDocument.NormalizedOperationWithAssumedSkipIncludeVariables::getNormalizedOperation)
400-
.orElseGet(normalizedDocument::getSingleNormalizedOperation);
401-
402-
if (instrument != null) {
403-
instrument.onCompleted(normalizedDocument, null);
404-
}
406+
return normalizedDocumentProvider.getNormalizedDocument(executionInput, this::createNormalizedDocument).thenApply(normalizedDocumentEntry -> {
407+
var normalizedDocument = normalizedDocumentEntry.getDocument();
408+
409+
// Search the document for the operation that matches the operationDefinition name,
410+
// if no match then it could be anonymous query, then fallback to the first operation.
411+
var normalizedOperations = normalizedDocument.getNormalizedOperations();
412+
var normalizedOperation = normalizedOperations.stream()
413+
.filter(this::isExecutingOperation)
414+
.findAny()
415+
.map(NormalizedDocument.NormalizedOperationWithAssumedSkipIncludeVariables::getNormalizedOperation)
416+
.orElseGet(normalizedDocument::getSingleNormalizedOperation);
417+
418+
if (instrument != null) {
419+
instrument.onCompleted(normalizedDocument, null);
420+
}
405421

406-
return normalizedOperation;
422+
return normalizedOperation;
423+
});
407424
}
408425

409426
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+
}

0 commit comments

Comments
 (0)