diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 5bdc076d36..fcf1663d8d 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -14,6 +14,7 @@ import graphql.execution.incremental.IncrementalCallState; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationCreateExecutableNormalizedOperationParameters; import graphql.language.Document; import graphql.language.FragmentDefinition; import graphql.language.OperationDefinition; @@ -100,7 +101,7 @@ public class ExecutionContext { this.localContext = builder.localContext; this.executionInput = builder.executionInput; this.dataLoaderDispatcherStrategy = builder.dataLoaderDispatcherStrategy; - this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables)); + this.queryTree = FpKit.interThreadMemoize(this::createExecutableNormalizedOperation); this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningState = builder.engineRunningState; } @@ -368,6 +369,16 @@ public ResultNodesInfo getResultNodesInfo() { return resultNodesInfo; } + private ExecutableNormalizedOperation createExecutableNormalizedOperation() { + var parameters = new InstrumentationCreateExecutableNormalizedOperationParameters(executionInput, graphQLSchema); + var instrument = instrumentation.beginCreateExecutableNormalizedOperation(parameters, instrumentationState); + var result = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables); + if (instrument != null) { + instrument.onCompleted(result, null); + } + return result; + } + @Internal public boolean hasIncrementalSupport() { GraphQLContext graphqlContext = getGraphQLContext(); diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index daca12293f..bc1f5cdfe3 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -8,6 +8,7 @@ import graphql.execution.Async; import graphql.execution.ExecutionContext; import graphql.execution.FieldValueInfo; +import graphql.execution.instrumentation.parameters.InstrumentationCreateExecutableNormalizedOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; @@ -17,6 +18,7 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.language.Document; +import graphql.normalized.ExecutableNormalizedOperation; import graphql.schema.DataFetcher; import graphql.schema.GraphQLSchema; import graphql.validation.ValidationError; @@ -126,6 +128,12 @@ public InstrumentationContext beginParse(InstrumentationExecutionParam return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginParse(parameters, specificState)); } + @Override + public InstrumentationContext beginCreateExecutableNormalizedOperation(InstrumentationCreateExecutableNormalizedOperationParameters parameters, InstrumentationState state) { + return chainedCtx(state, (instrumentation, specificState) -> + instrumentation.beginCreateExecutableNormalizedOperation(parameters, specificState)); + } + @Override public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 714f96fe3c..4949efdc58 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -12,8 +12,10 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters; +import graphql.execution.instrumentation.parameters.InstrumentationCreateExecutableNormalizedOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.language.Document; +import graphql.normalized.ExecutableNormalizedOperation; import graphql.schema.DataFetcher; import graphql.schema.GraphQLSchema; import graphql.validation.ValidationError; @@ -120,6 +122,19 @@ default InstrumentationContext beginExecuteOperation(Instrument return noOp(); } + /** + * This is called just before the creation of the executable normalized operation is started. + * + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createState(InstrumentationCreateStateParameters)} + * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + */ + @ExperimentalApi + @Nullable + default InstrumentationContext beginCreateExecutableNormalizedOperation(InstrumentationCreateExecutableNormalizedOperationParameters parameters, InstrumentationState state) { + return noOp(); + } + /** * This is called each time an {@link graphql.execution.ExecutionStrategy} is invoked, which may be multiple times * per query as the engine recursively descends over the query. diff --git a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java index 89e91b6e50..c8db929152 100644 --- a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java @@ -8,8 +8,10 @@ import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters; +import graphql.execution.instrumentation.parameters.InstrumentationCreateExecutableNormalizedOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.language.Document; +import graphql.normalized.ExecutableNormalizedOperation; import graphql.validation.ValidationError; import org.jspecify.annotations.Nullable; @@ -83,6 +85,11 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument return runAll(state, (instrumentation, specificState) -> instrumentation.beginExecuteObject(parameters, specificState)); } + @Override + public @Nullable InstrumentationContext beginCreateExecutableNormalizedOperation(InstrumentationCreateExecutableNormalizedOperationParameters parameters, InstrumentationState state) { + return runAll(state, (instrumentation, specificState) -> instrumentation.beginCreateExecutableNormalizedOperation(parameters, specificState)); + } + @Override public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { return runAll(state, (instrumentation, specificState) -> instrumentation.beginSubscribedFieldEvent(parameters, specificState)); diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationCreateExecutableNormalizedOperationParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationCreateExecutableNormalizedOperationParameters.java new file mode 100644 index 0000000000..7f0bdbb503 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationCreateExecutableNormalizedOperationParameters.java @@ -0,0 +1,17 @@ +package graphql.execution.instrumentation.parameters; + +import graphql.ExecutionInput; +import graphql.ExperimentalApi; +import graphql.execution.instrumentation.Instrumentation; +import graphql.schema.GraphQLSchema; + +/** + * Parameters sent to {@link Instrumentation} methods + */ +@SuppressWarnings("TypeParameterUnusedInFormals") +@ExperimentalApi +public class InstrumentationCreateExecutableNormalizedOperationParameters extends InstrumentationExecutionParameters { + public InstrumentationCreateExecutableNormalizedOperationParameters(ExecutionInput executionInput, GraphQLSchema schema) { + super(executionInput, schema); + } +} diff --git a/src/test/groovy/graphql/StarWarsData.groovy b/src/test/groovy/graphql/StarWarsData.groovy index d4a406d565..00fb552c71 100644 --- a/src/test/groovy/graphql/StarWarsData.groovy +++ b/src/test/groovy/graphql/StarWarsData.groovy @@ -105,6 +105,7 @@ class StarWarsData { static TypeResolver characterTypeResolver = new TypeResolver() { @Override GraphQLObjectType getType(TypeResolutionEnvironment env) { + env.getSelectionSet().getFields() // Used to validate instrumentation def id = env.getObject().id if (humanData[id] != null) return StarWarsSchema.humanType diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy index 88d7c86538..b6c0f15124 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy @@ -55,6 +55,8 @@ class ChainedInstrumentationStateTest extends Specification { "start:fetch-hero", "end:fetch-hero", "start:complete-hero", + "start:create-executable-normalized-operation", + "end:create-executable-normalized-operation", "start:execute-object", @@ -143,6 +145,8 @@ class ChainedInstrumentationStateTest extends Specification { "start:fetch-hero", "end:fetch-hero", "start:complete-hero", + "start:create-executable-normalized-operation", + "end:create-executable-normalized-operation", "start:execute-object", @@ -183,6 +187,8 @@ class ChainedInstrumentationStateTest extends Specification { "start:fetch-hero", "end:fetch-hero", "start:complete-hero", + "start:create-executable-normalized-operation", + "end:create-executable-normalized-operation", "start:execute-object", diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index 6580d59904..5463edeb10 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -312,6 +312,8 @@ class InstrumentationTest extends Specification { "onDispatched:fetch-hero", "end:fetch-hero", "start:complete-hero", + "start:create-executable-normalized-operation", + "end:create-executable-normalized-operation", "start:execute-object", "start:field-id", "start:fetch-id", diff --git a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy index 822d08f5a6..483459c8df 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy @@ -10,8 +10,10 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionStra import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters +import graphql.execution.instrumentation.parameters.InstrumentationCreateExecutableNormalizedOperationParameters import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters import graphql.language.Document +import graphql.normalized.ExecutableNormalizedOperation import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLSchema @@ -73,6 +75,12 @@ class ModernTestingInstrumentation implements Instrumentation { return new TestingInstrumentContext("execute-operation", executionList, throwableList, useOnDispatch) } + @Override + InstrumentationContext beginCreateExecutableNormalizedOperation(InstrumentationCreateExecutableNormalizedOperationParameters parameters, InstrumentationState state) { + assert state == instrumentationState + return new TestingInstrumentContext("create-executable-normalized-operation", executionList, throwableList, useOnDispatch) + } + @Override InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { assert state == instrumentationState diff --git a/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy index 5e17e040ad..3f79ab7d80 100644 --- a/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/NamedInstrumentation.groovy @@ -7,8 +7,10 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionPara import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters +import graphql.execution.instrumentation.parameters.InstrumentationCreateExecutableNormalizedOperationParameters import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters import graphql.language.Document +import graphql.normalized.ExecutableNormalizedOperation import graphql.schema.DataFetcher import graphql.validation.ValidationError @@ -72,6 +74,12 @@ class NamedInstrumentation extends ModernTestingInstrumentation { return super.beginFieldExecution(parameters, state) } + @Override + InstrumentationContext beginCreateExecutableNormalizedOperation(InstrumentationCreateExecutableNormalizedOperationParameters parameters, InstrumentationState state) { + assertState(state) + return super.beginCreateExecutableNormalizedOperation(parameters, state) + } + @Override InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { assertState(state) diff --git a/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy index 981f756633..f1556c57eb 100644 --- a/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/NoContextChainedInstrumentationTest.groovy @@ -41,6 +41,7 @@ class NoContextChainedInstrumentationTest extends Specification { "start:field-hero", "start:fetch-hero", "start:complete-hero", + "start:create-executable-normalized-operation", "start:execute-object",