From 9f65d6782afcc870b1639c64abe2f7cbd91dbcf0 Mon Sep 17 00:00:00 2001 From: bbaker Date: Fri, 6 Mar 2026 16:50:38 +1100 Subject: [PATCH 1/8] This adds support for QueryAppliedDirective on operations and documents --- .../java/graphql/execution/Execution.java | 16 +++++- .../graphql/execution/ExecutionContext.java | 22 ++++++- .../execution/ExecutionContextBuilder.java | 24 +++++--- .../directives/DirectivesResolver.java | 29 ++++++++++ .../OperationDirectivesResolver.java | 57 +++++++++++++++++++ .../directives/QueryDirectivesImpl.java | 19 +------ .../ExecutableNormalizedOperation.java | 18 ++++++ .../ExecutableNormalizedOperationFactory.java | 11 ++++ 8 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 src/main/java/graphql/execution/directives/OperationDirectivesResolver.java diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 38c82f5a53..d0804417a7 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -12,6 +12,8 @@ import graphql.GraphQLException; import graphql.Internal; import graphql.Profiler; +import graphql.execution.directives.OperationDirectivesResolver; +import graphql.execution.directives.QueryAppliedDirective; import graphql.execution.incremental.IncrementalCallState; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; @@ -40,6 +42,7 @@ import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; @@ -55,6 +58,7 @@ @Internal public class Execution { private final FieldCollector fieldCollector = new FieldCollector(); + private final OperationDirectivesResolver operationDirectivesResolver = new OperationDirectivesResolver(); private final ExecutionStrategy queryStrategy; private final ExecutionStrategy mutationStrategy; private final ExecutionStrategy subscriptionStrategy; @@ -100,9 +104,14 @@ public CompletableFuture execute(Document document, GraphQLSche boolean propagateErrorsOnNonNullContractFailure = propagateErrorsOnNonNullContractFailure(getOperationResult.operationDefinition.getDirectives()); - ResponseMapFactory responseMapFactory = GraphQL.unusualConfiguration(executionInput.getGraphQLContext()) + GraphQLContext graphQLContext = executionInput.getGraphQLContext(); + Locale locale = executionInput.getLocale(); + + ResponseMapFactory responseMapFactory = GraphQL.unusualConfiguration(graphQLContext) .responseMapFactory().getOr(ResponseMapFactory.DEFAULT); + Map> opDirectivesMap = operationDirectivesResolver.resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale); + ExecutionContext executionContext = newExecutionContextBuilder() .instrumentation(instrumentation) .instrumentationState(instrumentationState) @@ -112,7 +121,7 @@ public CompletableFuture execute(Document document, GraphQLSche .mutationStrategy(mutationStrategy) .subscriptionStrategy(subscriptionStrategy) .context(executionInput.getContext()) - .graphQLContext(executionInput.getGraphQLContext()) + .graphQLContext(graphQLContext) .localContext(executionInput.getLocalContext()) .root(executionInput.getRoot()) .fragmentsByName(getOperationResult.fragmentsByName) @@ -120,8 +129,9 @@ public CompletableFuture execute(Document document, GraphQLSche .normalizedVariableValues(normalizedVariableValues) .document(document) .operationDefinition(getOperationResult.operationDefinition) + .operationDirectives(opDirectivesMap) .dataLoaderRegistry(executionInput.getDataLoaderRegistry()) - .locale(executionInput.getLocale()) + .locale(locale) .valueUnboxer(valueUnboxer) .responseMapFactory(responseMapFactory) .executionInput(executionInput) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index ac4b1a8b0d..7ffd802062 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -11,6 +11,8 @@ import graphql.Profiler; import graphql.PublicApi; import graphql.collect.ImmutableKit; +import graphql.execution.directives.OperationDirectivesResolver; +import graphql.execution.directives.QueryAppliedDirective; import graphql.execution.incremental.IncrementalCallState; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; @@ -73,6 +75,7 @@ public class ExecutionContext { private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); private final EngineRunningState engineRunningState; + private final Map> opDirectivesMap; private final Profiler profiler; ExecutionContext(ExecutionContextBuilder builder) { @@ -102,6 +105,7 @@ public class ExecutionContext { this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables)); this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningState = builder.engineRunningState; + this.opDirectivesMap = builder.opDirectivesMap; this.profiler = builder.profiler; } @@ -137,6 +141,22 @@ public OperationDefinition getOperationDefinition() { return operationDefinition; } + /** + * @return the map of {@link QueryAppliedDirective}s by name that were on this executing operation + */ + public Map> getOperationDirectives() { + List list = opDirectivesMap.get(getOperationDefinition()); + return OperationDirectivesResolver.toAppliedDirectivesByName(list); + } + + /** + * @return the map of all the {@link QueryAppliedDirective}s that were on the {@link Document} including + * {@link OperationDefinition}s that are not currently executing. + */ + public Map> getAllOperationDirectives() { + return opDirectivesMap; + } + public CoercedVariables getCoercedVariables() { return coercedVariables; } @@ -156,7 +176,7 @@ public Supplier getNormalizedVariables() { * @deprecated use {@link #getGraphQLContext()} instead */ @Deprecated(since = "2021-07-05") - @SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" }) + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public @Nullable T getContext() { return (T) context; } diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index f8dd44898e..128f47962d 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -10,6 +10,7 @@ import graphql.Internal; import graphql.Profiler; import graphql.collect.ImmutableKit; +import graphql.execution.directives.QueryAppliedDirective; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationState; import graphql.language.Document; @@ -19,6 +20,8 @@ import org.dataloader.DataLoaderRegistry; import org.jspecify.annotations.Nullable; +import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Supplier; @@ -55,6 +58,7 @@ public class ExecutionContextBuilder { EngineRunningState engineRunningState; ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT; Profiler profiler; + Map> opDirectivesMap = Collections.emptyMap(); /** * @return a new builder of {@link graphql.execution.ExecutionContext}s @@ -168,6 +172,7 @@ public ExecutionContextBuilder root(Object root) { /** * @param variables map of already coerced variables + * * @return this builder * * @deprecated use {@link #coercedVariables(CoercedVariables)} instead @@ -246,13 +251,6 @@ public ExecutionContextBuilder propagapropagateErrorsOnNonNullContractFailureeEr return this; } - - public ExecutionContext build() { - // preconditions - assertNotNull(executionId, "You must provide a query identifier"); - return new ExecutionContext(this); - } - public ExecutionContextBuilder engineRunningState(EngineRunningState engineRunningState) { this.engineRunningState = engineRunningState; return this; @@ -262,4 +260,16 @@ public ExecutionContextBuilder profiler(Profiler profiler) { this.profiler = profiler; return this; } + + public ExecutionContextBuilder operationDirectives(Map> opDirectivesMap) { + this.opDirectivesMap = opDirectivesMap; + return this; + } + + + public ExecutionContext build() { + // preconditions + assertNotNull(executionId, "You must provide a query identifier"); + return new ExecutionContext(this); + } } diff --git a/src/main/java/graphql/execution/directives/DirectivesResolver.java b/src/main/java/graphql/execution/directives/DirectivesResolver.java index 4f177052e4..9419640198 100644 --- a/src/main/java/graphql/execution/directives/DirectivesResolver.java +++ b/src/main/java/graphql/execution/directives/DirectivesResolver.java @@ -3,6 +3,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.CoercedVariables; @@ -13,6 +14,7 @@ import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLSchema; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; @@ -61,4 +63,31 @@ private void buildArguments(GraphQLDirective.Builder directiveBuilder, } }); } + + public List toAppliedDirectives(List directives, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + BiMap directivesMap = resolveDirectives(directives, schema, variables, graphQLContext, locale); + return toAppliedDirectives(directivesMap.keySet()); + } + + public List toAppliedDirectives(Collection directives) { + return directives.stream().map(this::toAppliedDirective).collect(ImmutableList.toImmutableList()); + } + + public QueryAppliedDirective toAppliedDirective(GraphQLDirective directive) { + QueryAppliedDirective.Builder builder = QueryAppliedDirective.newDirective(); + builder.name(directive.getName()); + for (GraphQLArgument argument : directive.getArguments()) { + builder.argument(toAppliedArgument(argument)); + } + return builder.build(); + } + + public QueryAppliedDirectiveArgument toAppliedArgument(GraphQLArgument argument) { + return QueryAppliedDirectiveArgument.newArgument() + .name(argument.getName()) + .type(argument.getType()) + .inputValueWithState(argument.getArgumentValue()) + .build(); + } + } diff --git a/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java b/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java new file mode 100644 index 0000000000..74435af594 --- /dev/null +++ b/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java @@ -0,0 +1,57 @@ +package graphql.execution.directives; + +import com.google.common.collect.ImmutableMap; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.CoercedVariables; +import graphql.language.Document; +import graphql.language.OperationDefinition; +import graphql.schema.GraphQLSchema; +import org.jspecify.annotations.NullMarked; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Internal +@NullMarked +public class OperationDirectivesResolver { + + private final DirectivesResolver directivesResolver = new DirectivesResolver(); + + public Map> resolveDirectives(Document document, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + Map> map = new LinkedHashMap<>(); + List operations = document.getDefinitionsOfType(OperationDefinition.class); + for (OperationDefinition operationDefinition : operations) { + map.put(operationDefinition, resolveDirectives(operationDefinition, schema, variables, graphQLContext, locale)); + } + return ImmutableMap.copyOf(map); + } + + public List resolveDirectives(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + return directivesResolver.toAppliedDirectives( + operationDefinition.getDirectives(), + schema, + variables, + graphQLContext, + locale + ); + } + + public Map> resolveDirectiveByName(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + List list = resolveDirectives(operationDefinition, schema, variables, graphQLContext, locale); + return toAppliedDirectivesByName(list); + } + + public static Map> toAppliedDirectivesByName(List queryAppliedDirectives) { + Map> map = new LinkedHashMap<>(); + for (QueryAppliedDirective queryAppliedDirective : queryAppliedDirectives) { + List list = map.computeIfAbsent(queryAppliedDirective.getName(), k -> new ArrayList<>()); + list.add(queryAppliedDirective); + } + return ImmutableMap.copyOf(map); + } + +} diff --git a/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java b/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java index 87f00d6f97..c7c59ea092 100644 --- a/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java +++ b/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java @@ -80,7 +80,7 @@ private void computeValuesLazily() { ImmutableList.Builder appliedDirectiveBuilder = ImmutableList.builder(); for (GraphQLDirective resolvedDirective : resolvedDirectives) { - QueryAppliedDirective appliedDirective = toAppliedDirective(resolvedDirective); + QueryAppliedDirective appliedDirective = directivesResolver.toAppliedDirective(resolvedDirective); appliedDirectiveBuilder.add(appliedDirective); gqlDirectiveCounterParts.put(resolvedDirective, appliedDirective); } @@ -125,23 +125,6 @@ private void computeValuesLazily() { }); } - private QueryAppliedDirective toAppliedDirective(GraphQLDirective directive) { - QueryAppliedDirective.Builder builder = QueryAppliedDirective.newDirective(); - builder.name(directive.getName()); - for (GraphQLArgument argument : directive.getArguments()) { - builder.argument(toAppliedArgument(argument)); - } - return builder.build(); - } - - private QueryAppliedDirectiveArgument toAppliedArgument(GraphQLArgument argument) { - return QueryAppliedDirectiveArgument.newArgument() - .name(argument.getName()) - .type(argument.getType()) - .inputValueWithState(argument.getArgumentValue()) - .build(); - } - @Override public Map> getImmediateDirectivesByField() { diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index cfcda2746d..e50563fb22 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -5,6 +5,7 @@ import graphql.PublicApi; import graphql.execution.MergedField; import graphql.execution.ResultPath; +import graphql.execution.directives.QueryAppliedDirective; import graphql.execution.directives.QueryDirectives; import graphql.language.Field; import graphql.language.OperationDefinition; @@ -25,6 +26,7 @@ @PublicApi public class ExecutableNormalizedOperation { private final OperationDefinition.Operation operation; + private final Map> operationDirectives; private final String operationName; private final List topLevelFields; private final ImmutableListMultimap fieldToNormalizedField; @@ -37,6 +39,7 @@ public class ExecutableNormalizedOperation { public ExecutableNormalizedOperation( OperationDefinition.Operation operation, String operationName, + Map> operationDirectives, List topLevelFields, ImmutableListMultimap fieldToNormalizedField, Map normalizedFieldToMergedField, @@ -46,6 +49,7 @@ public ExecutableNormalizedOperation( int operationDepth) { this.operation = operation; this.operationName = operationName; + this.operationDirectives = operationDirectives; this.topLevelFields = topLevelFields; this.fieldToNormalizedField = fieldToNormalizedField; this.normalizedFieldToMergedField = normalizedFieldToMergedField; @@ -69,6 +73,20 @@ public String getOperationName() { return operationName; } + /** + * This is the directives that are on the operation itself and not the fields under it for example + *
+     * {@code
+     *   query opName @foo { field }
+     * }
+     * 
+ * + * @return the directives that are on this operation itself. + */ + public Map> getOperationDirectives() { + return operationDirectives; + } + /** * @return This returns how many {@link ExecutableNormalizedField}s are in the operation. */ diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 8501989237..07c0dbb587 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -16,6 +16,8 @@ import graphql.execution.RawVariables; import graphql.execution.ValuesResolver; import graphql.execution.conditional.ConditionalNodes; +import graphql.execution.directives.OperationDirectivesResolver; +import graphql.execution.directives.QueryAppliedDirective; import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; import graphql.execution.incremental.IncrementalUtils; @@ -449,6 +451,7 @@ private static class ExecutableNormalizedOperationFactoryImpl { private final CoercedVariables coercedVariableValues; private final @Nullable NormalizedVariables normalizedVariableValues; private final Options options; + private final OperationDirectivesResolver directivesResolver = new OperationDirectivesResolver(); private final List possibleMergerList = new ArrayList<>(); @@ -487,9 +490,17 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.deferSupport); } + + Map> operationDirectives = directivesResolver.resolveDirectiveByName(operationDefinition, + graphQLSchema, + coercedVariableValues, + options.graphQLContext, + options.locale); + return new ExecutableNormalizedOperation( operationDefinition.getOperation(), operationDefinition.getName(), + operationDirectives, new ArrayList<>(rootEnfs), fieldToNormalizedField.build(), normalizedFieldToMergedField.build(), From 17401626cea6a5dbd3f2b960350c4496cf1e6439 Mon Sep 17 00:00:00 2001 From: bbaker Date: Sat, 7 Mar 2026 09:43:08 +1100 Subject: [PATCH 2/8] This adds support for QueryAppliedDirective on operations and documents - tests added --- .../OperationDirectivesResolver.java | 2 +- .../ExecutableNormalizedOperationFactory.java | 2 +- .../OperationDirectivesResolverTest.groovy | 185 ++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy diff --git a/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java b/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java index 74435af594..97793acff4 100644 --- a/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java +++ b/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java @@ -40,7 +40,7 @@ public List resolveDirectives(OperationDefinition operati ); } - public Map> resolveDirectiveByName(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + public Map> resolveDirectivesByName(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { List list = resolveDirectives(operationDefinition, schema, variables, graphQLContext, locale); return toAppliedDirectivesByName(list); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 07c0dbb587..977f930117 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -491,7 +491,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.deferSupport); } - Map> operationDirectives = directivesResolver.resolveDirectiveByName(operationDefinition, + Map> operationDirectives = directivesResolver.resolveDirectivesByName(operationDefinition, graphQLSchema, coercedVariableValues, options.graphQLContext, diff --git a/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy b/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy new file mode 100644 index 0000000000..2483737994 --- /dev/null +++ b/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy @@ -0,0 +1,185 @@ +package graphql.execution.directives + +import graphql.ExecutionResult +import graphql.GraphQL +import graphql.GraphQLContext +import graphql.TestUtil +import graphql.execution.CoercedVariables +import graphql.execution.ExecutionContext +import graphql.execution.instrumentation.Instrumentation +import graphql.execution.instrumentation.InstrumentationContext +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters +import graphql.language.Document +import graphql.language.OperationDefinition +import graphql.schema.GraphQLScalarType +import spock.lang.Specification + +class OperationDirectivesResolverTest extends Specification { + + def schema = TestUtil.schema(""" + directive @foo on QUERY | MUTATION | SUBSCRIPTION + directive @bar on QUERY | MUTATION | SUBSCRIPTION + directive @baz repeatable on QUERY | MUTATION | SUBSCRIPTION + directive @timeout(ms : Int = -1) on QUERY | MUTATION | SUBSCRIPTION + + type Query { + f : String + } + type Mutation { + f : String + } + type Subscription { + f : String + } + """) + + def "can resolve out directives on a document"() { + + + def document = TestUtil.parseQuery(""" + query q1 @foo { + f + } + + query q2 @bar { + f + } + + mutation m1 @baz @baz { + f + } + + subscription s1 @timeout(ms : 100) { + f + } + + """) + + when: + Map> resolveDirectives = new OperationDirectivesResolver() + .resolveDirectives(document, schema, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()) + + def data = resolveDirectives.collectEntries { operation, directives -> + [operation.name, directives.collect { it.name }] // remap to names + } + then: + !resolveDirectives.isEmpty() + data["q1"] == ["foo"] + data["q2"] == ["bar"] + data["m1"] == ["baz", "baz"] + data["s1"] == ["timeout"] + } + + def "can resolve out directives on an operation"() { + + def document = TestUtil.parseQuery(""" + query q1 @timeout(ms : 100) @foo @bar { + f + } + """) + + def operationDefinition = extractOp(document) + + when: + Map> resolveDirectives = new OperationDirectivesResolver() + .resolveDirectivesByName(operationDefinition, schema, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()) + + then: + resolveDirectives.size() == 3 + def directives = resolveDirectives["timeout"] + directives.size() == 1 + + timeoutAsserts(directives[0], 100) + } + + def "can default values in directives"() { + def document = TestUtil.parseQuery(""" + query q1 @timeout @foo @bar { + f + } + """) + def operationDefinition = extractOp(document) + + when: + Map> resolveDirectives = new OperationDirectivesResolver() + .resolveDirectivesByName(operationDefinition, schema, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()) + + then: + resolveDirectives.size() == 3 + def directives = resolveDirectives["timeout"] + directives.size() == 1 + + timeoutAsserts(directives[0], -1) + + } + + + private static boolean timeoutAsserts(QueryAppliedDirective directive, Integer value) { + directive.name == "timeout" + directive.arguments.size() == 1 + directive.arguments[0].name == "ms" + (directive.arguments[0].type as GraphQLScalarType).name == "Int" + directive.arguments[0].value == value + true + } + + def "integration test"() { + + ExecutionContext executionContext = null + Instrumentation instrumentation = new Instrumentation() { + @Override + InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { + executionContext = parameters.getExecutionContext() + return null + } + } + + def graphQL = GraphQL.newGraphQL(schema).instrumentation(instrumentation).build() + + when: + graphQL.execute(""" + query q1 @timeout(ms : 100) @foo @bar @baz @baz { + f + } + """) + + then: + Map> resolveDirectives = executionContext.getOperationDirectives() + + commonIntegrationAsserts(resolveDirectives) + + when: + def normalizedOperation = executionContext.getNormalizedQueryTree().get() + def enoResolveDirectives = normalizedOperation.getOperationDirectives() + + then: + commonIntegrationAsserts(enoResolveDirectives) + + } + + private static boolean commonIntegrationAsserts(Map> resolveDirectives) { + assert resolveDirectives.size() == 4 + def directives = resolveDirectives["timeout"] + assert directives.size() == 1 + + def directive = directives[0] + assert directive.name == "timeout" + assert directive.arguments.size() == 1 + assert directive.arguments[0].name == "ms" + assert (directive.arguments[0].type as GraphQLScalarType).name == "Int" + assert directive.arguments[0].value == 100 + + assert resolveDirectives["foo"].size() == 1 + assert resolveDirectives["bar"].size() == 1 + assert resolveDirectives["baz"].size() == 2 + + true + + } + + private static OperationDefinition extractOp(Document document) { + document.getDefinitionsOfType(OperationDefinition.class)[0] + } + +} From 1951ed6e98c72541d1d6c6efb8aa719d2109af28 Mon Sep 17 00:00:00 2001 From: bbaker Date: Sat, 7 Mar 2026 09:49:08 +1100 Subject: [PATCH 3/8] This adds support for QueryAppliedDirective on operations and documents - refactor --- .../execution/directives/DirectivesResolver.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/graphql/execution/directives/DirectivesResolver.java b/src/main/java/graphql/execution/directives/DirectivesResolver.java index 9419640198..c6f7febe4c 100644 --- a/src/main/java/graphql/execution/directives/DirectivesResolver.java +++ b/src/main/java/graphql/execution/directives/DirectivesResolver.java @@ -3,9 +3,9 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableBiMap; -import com.google.common.collect.ImmutableList; import graphql.GraphQLContext; import graphql.Internal; +import graphql.collect.ImmutableKit; import graphql.execution.CoercedVariables; import graphql.execution.ValuesResolver; import graphql.language.Directive; @@ -14,7 +14,6 @@ import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLSchema; -import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; @@ -66,13 +65,16 @@ private void buildArguments(GraphQLDirective.Builder directiveBuilder, public List toAppliedDirectives(List directives, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { BiMap directivesMap = resolveDirectives(directives, schema, variables, graphQLContext, locale); - return toAppliedDirectives(directivesMap.keySet()); - } - - public List toAppliedDirectives(Collection directives) { - return directives.stream().map(this::toAppliedDirective).collect(ImmutableList.toImmutableList()); + return ImmutableKit.map(directivesMap.keySet(), this::toAppliedDirective); } + /** + * This helps us remodel the applied GraphQLDirective back to the better modelled and named {@link QueryAppliedDirective} + * + * @param directive the directive to remodel + * + * @return a QueryAppliedDirective + */ public QueryAppliedDirective toAppliedDirective(GraphQLDirective directive) { QueryAppliedDirective.Builder builder = QueryAppliedDirective.newDirective(); builder.name(directive.getName()); From 2e7c4fc169f34afb16a19ddc70affbf3cb9b7eff Mon Sep 17 00:00:00 2001 From: bbaker Date: Sat, 7 Mar 2026 10:36:30 +1100 Subject: [PATCH 4/8] This adds support for QueryAppliedDirective on operations and documents - use Immutable signature --- .../java/graphql/execution/Execution.java | 3 +- .../graphql/execution/ExecutionContext.java | 6 ++-- .../execution/ExecutionContextBuilder.java | 4 +-- .../directives/DirectivesResolver.java | 3 +- .../OperationDirectivesResolver.java | 29 ++++++++----------- .../ExecutableNormalizedOperation.java | 7 +++-- .../ExecutableNormalizedOperationFactory.java | 2 +- .../OperationDirectivesResolverTest.groovy | 21 +++++++------- 8 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index d0804417a7..88cb232141 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -1,6 +1,7 @@ package graphql.execution; +import com.google.common.collect.ImmutableList; import graphql.Directives; import graphql.EngineRunningState; import graphql.ExecutionInput; @@ -110,7 +111,7 @@ public CompletableFuture execute(Document document, GraphQLSche ResponseMapFactory responseMapFactory = GraphQL.unusualConfiguration(graphQLContext) .responseMapFactory().getOr(ResponseMapFactory.DEFAULT); - Map> opDirectivesMap = operationDirectivesResolver.resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale); + Map> opDirectivesMap = operationDirectivesResolver.resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale); ExecutionContext executionContext = newExecutionContextBuilder() .instrumentation(instrumentation) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 7ffd802062..1562ac8b5e 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -75,7 +75,7 @@ public class ExecutionContext { private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); private final EngineRunningState engineRunningState; - private final Map> opDirectivesMap; + private final Map> opDirectivesMap; private final Profiler profiler; ExecutionContext(ExecutionContextBuilder builder) { @@ -144,7 +144,7 @@ public OperationDefinition getOperationDefinition() { /** * @return the map of {@link QueryAppliedDirective}s by name that were on this executing operation */ - public Map> getOperationDirectives() { + public Map> getOperationDirectives() { List list = opDirectivesMap.get(getOperationDefinition()); return OperationDirectivesResolver.toAppliedDirectivesByName(list); } @@ -153,7 +153,7 @@ public Map> getOperationDirectives() { * @return the map of all the {@link QueryAppliedDirective}s that were on the {@link Document} including * {@link OperationDefinition}s that are not currently executing. */ - public Map> getAllOperationDirectives() { + public Map> getAllOperationDirectives() { return opDirectivesMap; } diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index 128f47962d..2ca5b4cc43 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -58,7 +58,7 @@ public class ExecutionContextBuilder { EngineRunningState engineRunningState; ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT; Profiler profiler; - Map> opDirectivesMap = Collections.emptyMap(); + Map> opDirectivesMap = Collections.emptyMap(); /** * @return a new builder of {@link graphql.execution.ExecutionContext}s @@ -261,7 +261,7 @@ public ExecutionContextBuilder profiler(Profiler profiler) { return this; } - public ExecutionContextBuilder operationDirectives(Map> opDirectivesMap) { + public ExecutionContextBuilder operationDirectives(Map> opDirectivesMap) { this.opDirectivesMap = opDirectivesMap; return this; } diff --git a/src/main/java/graphql/execution/directives/DirectivesResolver.java b/src/main/java/graphql/execution/directives/DirectivesResolver.java index c6f7febe4c..9fff2a514c 100644 --- a/src/main/java/graphql/execution/directives/DirectivesResolver.java +++ b/src/main/java/graphql/execution/directives/DirectivesResolver.java @@ -3,6 +3,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; import graphql.GraphQLContext; import graphql.Internal; import graphql.collect.ImmutableKit; @@ -63,7 +64,7 @@ private void buildArguments(GraphQLDirective.Builder directiveBuilder, }); } - public List toAppliedDirectives(List directives, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + public ImmutableList toAppliedDirectives(List directives, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { BiMap directivesMap = resolveDirectives(directives, schema, variables, graphQLContext, locale); return ImmutableKit.map(directivesMap.keySet(), this::toAppliedDirective); } diff --git a/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java b/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java index 97793acff4..b437593318 100644 --- a/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java +++ b/src/main/java/graphql/execution/directives/OperationDirectivesResolver.java @@ -1,5 +1,6 @@ package graphql.execution.directives; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.GraphQLContext; import graphql.Internal; @@ -7,10 +8,9 @@ import graphql.language.Document; import graphql.language.OperationDefinition; import graphql.schema.GraphQLSchema; +import graphql.util.FpKit; import org.jspecify.annotations.NullMarked; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -21,16 +21,15 @@ public class OperationDirectivesResolver { private final DirectivesResolver directivesResolver = new DirectivesResolver(); - public Map> resolveDirectives(Document document, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { - Map> map = new LinkedHashMap<>(); - List operations = document.getDefinitionsOfType(OperationDefinition.class); - for (OperationDefinition operationDefinition : operations) { - map.put(operationDefinition, resolveDirectives(operationDefinition, schema, variables, graphQLContext, locale)); + public ImmutableMap> resolveDirectives(Document document, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (OperationDefinition operationDefinition : document.getDefinitionsOfType(OperationDefinition.class)) { + builder.put(operationDefinition, resolveDirectives(operationDefinition, schema, variables, graphQLContext, locale)); } - return ImmutableMap.copyOf(map); + return builder.build(); } - public List resolveDirectives(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + public ImmutableList resolveDirectives(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { return directivesResolver.toAppliedDirectives( operationDefinition.getDirectives(), schema, @@ -40,18 +39,14 @@ public List resolveDirectives(OperationDefinition operati ); } - public Map> resolveDirectivesByName(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { + public ImmutableMap> resolveDirectivesByName(OperationDefinition operationDefinition, GraphQLSchema schema, CoercedVariables variables, GraphQLContext graphQLContext, Locale locale) { List list = resolveDirectives(operationDefinition, schema, variables, graphQLContext, locale); return toAppliedDirectivesByName(list); } - public static Map> toAppliedDirectivesByName(List queryAppliedDirectives) { - Map> map = new LinkedHashMap<>(); - for (QueryAppliedDirective queryAppliedDirective : queryAppliedDirectives) { - List list = map.computeIfAbsent(queryAppliedDirective.getName(), k -> new ArrayList<>()); - list.add(queryAppliedDirective); - } - return ImmutableMap.copyOf(map); + public static ImmutableMap> toAppliedDirectivesByName(List queryAppliedDirectives) { + Map> immutableListMap = FpKit.groupingBy(queryAppliedDirectives, QueryAppliedDirective::getName); + return ImmutableMap.copyOf(immutableListMap); } } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index e50563fb22..2200d02777 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -1,5 +1,6 @@ package graphql.normalized; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import graphql.Assert; import graphql.PublicApi; @@ -26,7 +27,7 @@ @PublicApi public class ExecutableNormalizedOperation { private final OperationDefinition.Operation operation; - private final Map> operationDirectives; + private final Map> operationDirectives; private final String operationName; private final List topLevelFields; private final ImmutableListMultimap fieldToNormalizedField; @@ -39,7 +40,7 @@ public class ExecutableNormalizedOperation { public ExecutableNormalizedOperation( OperationDefinition.Operation operation, String operationName, - Map> operationDirectives, + Map> operationDirectives, List topLevelFields, ImmutableListMultimap fieldToNormalizedField, Map normalizedFieldToMergedField, @@ -83,7 +84,7 @@ public String getOperationName() { * * @return the directives that are on this operation itself. */ - public Map> getOperationDirectives() { + public Map> getOperationDirectives() { return operationDirectives; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 977f930117..8050f5154c 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -491,7 +491,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.deferSupport); } - Map> operationDirectives = directivesResolver.resolveDirectivesByName(operationDefinition, + Map> operationDirectives = directivesResolver.resolveDirectivesByName(operationDefinition, graphQLSchema, coercedVariableValues, options.graphQLContext, diff --git a/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy b/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy index 2483737994..bf2fd75062 100644 --- a/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy @@ -1,5 +1,6 @@ package graphql.execution.directives +import com.google.common.collect.ImmutableList import graphql.ExecutionResult import graphql.GraphQL import graphql.GraphQLContext @@ -57,7 +58,7 @@ class OperationDirectivesResolverTest extends Specification { """) when: - Map> resolveDirectives = new OperationDirectivesResolver() + def resolveDirectives = new OperationDirectivesResolver() .resolveDirectives(document, schema, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()) def data = resolveDirectives.collectEntries { operation, directives -> @@ -82,7 +83,7 @@ class OperationDirectivesResolverTest extends Specification { def operationDefinition = extractOp(document) when: - Map> resolveDirectives = new OperationDirectivesResolver() + def resolveDirectives = new OperationDirectivesResolver() .resolveDirectivesByName(operationDefinition, schema, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()) then: @@ -102,7 +103,7 @@ class OperationDirectivesResolverTest extends Specification { def operationDefinition = extractOp(document) when: - Map> resolveDirectives = new OperationDirectivesResolver() + def resolveDirectives = new OperationDirectivesResolver() .resolveDirectivesByName(operationDefinition, schema, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()) then: @@ -116,11 +117,11 @@ class OperationDirectivesResolverTest extends Specification { private static boolean timeoutAsserts(QueryAppliedDirective directive, Integer value) { - directive.name == "timeout" - directive.arguments.size() == 1 - directive.arguments[0].name == "ms" - (directive.arguments[0].type as GraphQLScalarType).name == "Int" - directive.arguments[0].value == value + assert directive.name == "timeout" + assert directive.arguments.size() == 1 + assert directive.arguments[0].name == "ms" + assert (directive.arguments[0].type as GraphQLScalarType).name == "Int" + assert directive.arguments[0].value == value true } @@ -145,7 +146,7 @@ class OperationDirectivesResolverTest extends Specification { """) then: - Map> resolveDirectives = executionContext.getOperationDirectives() + def resolveDirectives = executionContext.getOperationDirectives() commonIntegrationAsserts(resolveDirectives) @@ -158,7 +159,7 @@ class OperationDirectivesResolverTest extends Specification { } - private static boolean commonIntegrationAsserts(Map> resolveDirectives) { + private static boolean commonIntegrationAsserts(Map> resolveDirectives) { assert resolveDirectives.size() == 4 def directives = resolveDirectives["timeout"] assert directives.size() == 1 From 8f049fe2332e43b0b4d054f507e5a984c623fadd Mon Sep 17 00:00:00 2001 From: bbaker Date: Sat, 7 Mar 2026 10:44:26 +1100 Subject: [PATCH 5/8] This adds support for QueryAppliedDirective on operations and documents - used field to make it cheaper --- src/main/java/graphql/execution/Execution.java | 5 +++-- .../java/graphql/execution/ExecutionContext.java | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 88cb232141..7c4fd44b0e 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -111,7 +111,8 @@ public CompletableFuture execute(Document document, GraphQLSche ResponseMapFactory responseMapFactory = GraphQL.unusualConfiguration(graphQLContext) .responseMapFactory().getOr(ResponseMapFactory.DEFAULT); - Map> opDirectivesMap = operationDirectivesResolver.resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale); + Map> operationDirectives = operationDirectivesResolver + .resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale); ExecutionContext executionContext = newExecutionContextBuilder() .instrumentation(instrumentation) @@ -130,7 +131,7 @@ public CompletableFuture execute(Document document, GraphQLSche .normalizedVariableValues(normalizedVariableValues) .document(document) .operationDefinition(getOperationResult.operationDefinition) - .operationDirectives(opDirectivesMap) + .operationDirectives(operationDirectives) .dataLoaderRegistry(executionInput.getDataLoaderRegistry()) .locale(locale) .valueUnboxer(valueUnboxer) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 1562ac8b5e..0abe6a0ec8 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -75,7 +75,8 @@ public class ExecutionContext { private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); private final EngineRunningState engineRunningState; - private final Map> opDirectivesMap; + private final Map> allOperationsDirectives; + private final Map> operationDirectives; private final Profiler profiler; ExecutionContext(ExecutionContextBuilder builder) { @@ -105,8 +106,10 @@ public class ExecutionContext { this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables)); this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningState = builder.engineRunningState; - this.opDirectivesMap = builder.opDirectivesMap; this.profiler = builder.profiler; + this.allOperationsDirectives = builder.opDirectivesMap; + List list = allOperationsDirectives.get(getOperationDefinition()); + this.operationDirectives = OperationDirectivesResolver.toAppliedDirectivesByName(list); } public ExecutionId getExecutionId() { @@ -145,8 +148,7 @@ public OperationDefinition getOperationDefinition() { * @return the map of {@link QueryAppliedDirective}s by name that were on this executing operation */ public Map> getOperationDirectives() { - List list = opDirectivesMap.get(getOperationDefinition()); - return OperationDirectivesResolver.toAppliedDirectivesByName(list); + return operationDirectives; } /** @@ -154,7 +156,7 @@ public Map> getOperationDirectives( * {@link OperationDefinition}s that are not currently executing. */ public Map> getAllOperationDirectives() { - return opDirectivesMap; + return allOperationsDirectives; } public CoercedVariables getCoercedVariables() { From 17e7ccd1b9323c1eea35c894c4691a0cc08d1f49 Mon Sep 17 00:00:00 2001 From: bbaker Date: Sat, 7 Mar 2026 12:07:23 +1100 Subject: [PATCH 6/8] This adds support for QueryAppliedDirective on operations and documents - made it a supplier for performance --- .../java/graphql/execution/Execution.java | 4 +-- .../graphql/execution/ExecutionContext.java | 32 ++++++++++++++----- .../execution/ExecutionContextBuilder.java | 7 ++-- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 7c4fd44b0e..448d1c7388 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -111,8 +111,8 @@ public CompletableFuture execute(Document document, GraphQLSche ResponseMapFactory responseMapFactory = GraphQL.unusualConfiguration(graphQLContext) .responseMapFactory().getOr(ResponseMapFactory.DEFAULT); - Map> operationDirectives = operationDirectivesResolver - .resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale); + Supplier>> operationDirectives = FpKit.interThreadMemoize(() -> + operationDirectivesResolver.resolveDirectives(document, graphQLSchema, coercedVariables, graphQLContext, locale)); ExecutionContext executionContext = newExecutionContextBuilder() .instrumentation(instrumentation) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 0abe6a0ec8..21cf26fab5 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -36,6 +36,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import static graphql.normalized.ExecutableNormalizedOperationFactory.*; + @SuppressWarnings("TypeParameterUnusedInFormals") @PublicApi public class ExecutionContext { @@ -75,8 +77,8 @@ public class ExecutionContext { private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo(); private final EngineRunningState engineRunningState; - private final Map> allOperationsDirectives; - private final Map> operationDirectives; + private final Supplier>> allOperationsDirectives; + private final Supplier>> operationDirectives; private final Profiler profiler; ExecutionContext(ExecutionContextBuilder builder) { @@ -103,13 +105,27 @@ 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.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure; this.engineRunningState = builder.engineRunningState; this.profiler = builder.profiler; - this.allOperationsDirectives = builder.opDirectivesMap; - List list = allOperationsDirectives.get(getOperationDefinition()); - this.operationDirectives = OperationDirectivesResolver.toAppliedDirectivesByName(list); + // lazy loading for performance + this.queryTree = mkExecutableNormalizedOperation(); + this.allOperationsDirectives = builder.allOperationsDirectives; + this.operationDirectives = mkOpDirectives(builder.allOperationsDirectives); + } + + private Supplier mkExecutableNormalizedOperation() { + return FpKit.interThreadMemoize(() -> { + Options options = Options.defaultOptions().graphQLContext(graphQLContext).locale(locale); + return createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables, options); + }); + } + + private Supplier>> mkOpDirectives(Supplier>> allOperationsDirectives) { + return FpKit.interThreadMemoize(() -> { + List list = allOperationsDirectives.get().get(operationDefinition); + return OperationDirectivesResolver.toAppliedDirectivesByName(list); + }); } public ExecutionId getExecutionId() { @@ -148,7 +164,7 @@ public OperationDefinition getOperationDefinition() { * @return the map of {@link QueryAppliedDirective}s by name that were on this executing operation */ public Map> getOperationDirectives() { - return operationDirectives; + return operationDirectives.get(); } /** @@ -156,7 +172,7 @@ public Map> getOperationDirectives( * {@link OperationDefinition}s that are not currently executing. */ public Map> getAllOperationDirectives() { - return allOperationsDirectives; + return allOperationsDirectives.get(); } public CoercedVariables getCoercedVariables() { diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index 2ca5b4cc43..4077b8def3 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -21,7 +21,6 @@ import org.jspecify.annotations.Nullable; import java.util.Collections; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Supplier; @@ -58,7 +57,7 @@ public class ExecutionContextBuilder { EngineRunningState engineRunningState; ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT; Profiler profiler; - Map> opDirectivesMap = Collections.emptyMap(); + Supplier>> allOperationsDirectives = Collections::emptyMap; /** * @return a new builder of {@link graphql.execution.ExecutionContext}s @@ -261,8 +260,8 @@ public ExecutionContextBuilder profiler(Profiler profiler) { return this; } - public ExecutionContextBuilder operationDirectives(Map> opDirectivesMap) { - this.opDirectivesMap = opDirectivesMap; + public ExecutionContextBuilder operationDirectives(Supplier>> allOperationsDirectives) { + this.allOperationsDirectives = allOperationsDirectives; return this; } From fdd95684f8530f217f74e1a88ebd51d2d1598cdb Mon Sep 17 00:00:00 2001 From: bbaker Date: Sat, 7 Mar 2026 21:37:41 +1100 Subject: [PATCH 7/8] This adds support for QueryAppliedDirective on operations and documents -added more tests --- .../OperationDirectivesResolverTest.groovy | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy b/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy index bf2fd75062..9f5de2632c 100644 --- a/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/directives/OperationDirectivesResolverTest.groovy @@ -1,6 +1,7 @@ package graphql.execution.directives import com.google.common.collect.ImmutableList +import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQL import graphql.GraphQLContext @@ -14,6 +15,7 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperat import graphql.language.Document import graphql.language.OperationDefinition import graphql.schema.GraphQLScalarType +import graphql.util.FpKit import spock.lang.Specification class OperationDirectivesResolverTest extends Specification { @@ -139,11 +141,17 @@ class OperationDirectivesResolverTest extends Specification { def graphQL = GraphQL.newGraphQL(schema).instrumentation(instrumentation).build() when: - graphQL.execute(""" + def ei = ExecutionInput.newExecutionInput(""" query q1 @timeout(ms : 100) @foo @bar @baz @baz { f } - """) + + mutation m1 @timeout(ms : 100) @foo @bar @baz @baz { + f + } + """).operationName("q1").build() + graphQL.execute(ei) + then: def resolveDirectives = executionContext.getOperationDirectives() @@ -157,6 +165,15 @@ class OperationDirectivesResolverTest extends Specification { then: commonIntegrationAsserts(enoResolveDirectives) + when: + def allOperationDirectives = executionContext.getAllOperationDirectives() + + then: + allOperationDirectives.size() == 2 + ImmutableList firstList = allOperationDirectives.values().iterator().next() + def firstResolvedDirectives = FpKit.groupingBy(firstList, { it -> it.name }) + commonIntegrationAsserts(firstResolvedDirectives) + } private static boolean commonIntegrationAsserts(Map> resolveDirectives) { From 601195a2515008fa233b4b496df893e22e064aff Mon Sep 17 00:00:00 2001 From: bbaker Date: Mon, 9 Mar 2026 10:41:34 +1100 Subject: [PATCH 8/8] This adds support for QueryAppliedDirective on operations and documents - PR feedback --- src/main/java/graphql/execution/ExecutionContext.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index 21cf26fab5..51a72b56dc 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -20,7 +20,6 @@ import graphql.language.FragmentDefinition; import graphql.language.OperationDefinition; import graphql.normalized.ExecutableNormalizedOperation; -import graphql.normalized.ExecutableNormalizedOperationFactory; import graphql.schema.GraphQLSchema; import graphql.util.FpKit; import graphql.util.LockKit; @@ -36,7 +35,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; -import static graphql.normalized.ExecutableNormalizedOperationFactory.*; +import static graphql.normalized.ExecutableNormalizedOperationFactory.Options; +import static graphql.normalized.ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation; @SuppressWarnings("TypeParameterUnusedInFormals") @PublicApi