Skip to content

Commit 3557a71

Browse files
committed
1 parent b714aa4 commit 3557a71

10 files changed

+136
-64
lines changed

src/main/java/graphql/Directives.java

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
11
package graphql;
22

33

4-
import graphql.language.BooleanValue;
5-
import graphql.language.Description;
6-
import graphql.language.DirectiveDefinition;
7-
import graphql.language.StringValue;
4+
import graphql.language.*;
85
import graphql.schema.GraphQLDirective;
6+
import graphql.schema.GraphQLEnumType;
97

108
import static graphql.Scalars.GraphQLBoolean;
119
import static graphql.Scalars.GraphQLString;
12-
import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION;
13-
import static graphql.introspection.Introspection.DirectiveLocation.ENUM_VALUE;
14-
import static graphql.introspection.Introspection.DirectiveLocation.FIELD;
15-
import static graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION;
16-
import static graphql.introspection.Introspection.DirectiveLocation.FRAGMENT_SPREAD;
17-
import static graphql.introspection.Introspection.DirectiveLocation.INLINE_FRAGMENT;
18-
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION;
19-
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT;
20-
import static graphql.introspection.Introspection.DirectiveLocation.SCALAR;
10+
import static graphql.introspection.Introspection.DirectiveLocation.*;
2111
import static graphql.language.DirectiveLocation.newDirectiveLocation;
2212
import static graphql.language.InputValueDefinition.newInputValueDefinition;
2313
import static graphql.language.NonNullType.newNonNullType;
@@ -37,6 +27,7 @@ public class Directives {
3727
private static final String SPECIFIED_BY = "specifiedBy";
3828
private static final String ONE_OF = "oneOf";
3929
private static final String DEFER = "defer";
30+
private static final String ERROR_HANDLING = "errorHandling";
4031

4132
public static final DirectiveDefinition DEPRECATED_DIRECTIVE_DEFINITION;
4233
public static final DirectiveDefinition INCLUDE_DIRECTIVE_DEFINITION;
@@ -46,6 +37,8 @@ public class Directives {
4637
public static final DirectiveDefinition ONE_OF_DIRECTIVE_DEFINITION;
4738
@ExperimentalApi
4839
public static final DirectiveDefinition DEFER_DIRECTIVE_DEFINITION;
40+
@ExperimentalApi
41+
public static final DirectiveDefinition ERROR_HANDLING_DIRECTIVE_DEFINITION;
4942

5043
public static final String BOOLEAN = "Boolean";
5144
public static final String STRING = "String";
@@ -133,6 +126,19 @@ public class Directives {
133126
.type(newTypeName().name(STRING).build())
134127
.build())
135128
.build();
129+
ERROR_HANDLING_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition()
130+
.name(ERROR_HANDLING)
131+
.directiveLocation(newDirectiveLocation().name(QUERY.name()).build())
132+
.directiveLocation(newDirectiveLocation().name(MUTATION.name()).build())
133+
.directiveLocation(newDirectiveLocation().name(SUBSCRIPTION.name()).build())
134+
.description(createDescription("This directive controls how to handle errors"))
135+
.inputValueDefinition(
136+
newInputValueDefinition()
137+
.name("onError")
138+
.type(newNonNullType(newTypeName().name(Enums.ON_ERROR).build()).build())
139+
.defaultValue(EnumValue.newEnumValue(Enums.ON_ERROR_PROPAGATE).build())
140+
.build())
141+
.build();
136142
}
137143

138144
/**
@@ -226,6 +232,23 @@ public class Directives {
226232
.definition(ONE_OF_DIRECTIVE_DEFINITION)
227233
.build();
228234

235+
@ExperimentalApi
236+
public static final GraphQLDirective ErrorHandlingDirective = GraphQLDirective.newDirective()
237+
.name(ERROR_HANDLING)
238+
.description("This directive controls how to handle errors.")
239+
.argument(newArgument()
240+
.name("onError")
241+
.type(nonNull(GraphQLEnumType.newEnum()
242+
.name(Enums.ON_ERROR)
243+
.value(Enums.ON_ERROR_PROPAGATE)
244+
.value(Enums.ON_ERROR_NULL)
245+
.build()))
246+
.defaultValueProgrammatic(Enums.ON_ERROR_PROPAGATE)
247+
.description("The URL that specifies the behaviour of this scalar."))
248+
.validLocations(QUERY, MUTATION, SUBSCRIPTION)
249+
.definition(ERROR_HANDLING_DIRECTIVE_DEFINITION)
250+
.build();
251+
229252
private static Description createDescription(String s) {
230253
return new Description(s, null, false);
231254
}

src/main/java/graphql/Enums.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package graphql;
2+
3+
import graphql.language.Description;
4+
import graphql.language.EnumTypeDefinition;
5+
import graphql.language.EnumValueDefinition;
6+
7+
/**
8+
* The enums that are understood by graphql-java
9+
*/
10+
public class Enums {
11+
public static final String ON_ERROR = "OnError";
12+
public static final String ON_ERROR_PROPAGATE = "PROPAGATE";
13+
public static final String ON_ERROR_NULL = "NULL";
14+
15+
@ExperimentalApi
16+
public static final EnumTypeDefinition ON_ERROR_TYPE_DEFINITION;
17+
18+
static {
19+
ON_ERROR_TYPE_DEFINITION = EnumTypeDefinition.newEnumTypeDefinition()
20+
.name(ON_ERROR)
21+
.enumValueDefinition(
22+
EnumValueDefinition.newEnumValueDefinition()
23+
.name(ON_ERROR_PROPAGATE)
24+
.description(new Description("If the error happens in a non-nullable position, the error is propagated to the neareast nullable parent position", null, true))
25+
.build()
26+
)
27+
.enumValueDefinition(
28+
EnumValueDefinition.newEnumValueDefinition()
29+
.name(ON_ERROR_NULL)
30+
.description(new Description("Null is always returned regardless of whether the position is nullable or not", null, true))
31+
.build()
32+
)
33+
.build();
34+
}
35+
}

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
package graphql.execution;
22

33

4-
import graphql.ExecutionInput;
5-
import graphql.ExecutionResult;
6-
import graphql.ExecutionResultImpl;
7-
import graphql.ExperimentalApi;
8-
import graphql.GraphQLContext;
9-
import graphql.GraphQLError;
10-
import graphql.Internal;
4+
import graphql.*;
115
import graphql.execution.incremental.IncrementalCallState;
126
import graphql.execution.instrumentation.Instrumentation;
137
import graphql.execution.instrumentation.InstrumentationContext;
@@ -20,22 +14,16 @@
2014
import graphql.extensions.ExtensionsBuilder;
2115
import graphql.incremental.DelayedIncrementalPartialResult;
2216
import graphql.incremental.IncrementalExecutionResultImpl;
23-
import graphql.language.Document;
24-
import graphql.language.FragmentDefinition;
25-
import graphql.language.NodeUtil;
26-
import graphql.language.OperationDefinition;
27-
import graphql.language.VariableDefinition;
17+
import graphql.language.*;
2818
import graphql.schema.GraphQLObjectType;
2919
import graphql.schema.GraphQLSchema;
3020
import graphql.schema.impl.SchemaUtil;
3121
import org.reactivestreams.Publisher;
3222

33-
import java.util.Collections;
34-
import java.util.List;
35-
import java.util.Map;
36-
import java.util.Optional;
23+
import java.util.*;
3724
import java.util.concurrent.CompletableFuture;
3825

26+
import static graphql.Directives.*;
3927
import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder;
4028
import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo;
4129
import static graphql.execution.ExecutionStrategyParameters.newParameters;
@@ -106,6 +94,7 @@ public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSche
10694
.locale(executionInput.getLocale())
10795
.valueUnboxer(valueUnboxer)
10896
.executionInput(executionInput)
97+
.propagateErrors(propagateErrors(coercedVariables, operationDefinition.getDirectives(), true))
10998
.build();
11099

111100
executionContext.getGraphQLContext().put(ResultNodesInfo.RESULT_NODES_INFO, executionContext.getResultNodesInfo());
@@ -262,4 +251,15 @@ private ExecutionResult mergeExtensionsBuilderIfPresent(ExecutionResult executio
262251
}
263252
return executionResult;
264253
}
254+
255+
private boolean propagateErrors(CoercedVariables variables, List<Directive> directives, boolean defaultValue) {
256+
Directive foundDirective = NodeUtil.findNodeByName(directives, ERROR_HANDLING_DIRECTIVE_DEFINITION.getName());
257+
if (foundDirective != null) {
258+
Map<String, Object> argumentValues = ValuesResolver.getArgumentValues(ErrorHandlingDirective.getArguments(), foundDirective.getArguments(), variables, GraphQLContext.getDefault(), Locale.getDefault());
259+
Object flag = argumentValues.get("onError");
260+
Assert.assertTrue(flag instanceof String, "The '%s' directive MUST have a OnError value for the 'if' argument", ERROR_HANDLING_DIRECTIVE_DEFINITION.getName());
261+
return flag.equals(Enums.ON_ERROR_PROPAGATE);
262+
}
263+
return defaultValue;
264+
}
265265
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33

44
import com.google.common.collect.ImmutableList;
55
import com.google.common.collect.ImmutableMap;
6-
import graphql.ExecutionInput;
7-
import graphql.GraphQLContext;
8-
import graphql.GraphQLError;
9-
import graphql.Internal;
10-
import graphql.PublicApi;
6+
import graphql.*;
117
import graphql.collect.ImmutableKit;
128
import graphql.execution.incremental.IncrementalCallState;
139
import graphql.execution.instrumentation.Instrumentation;
@@ -59,6 +55,7 @@ public class ExecutionContext {
5955
private final ValueUnboxer valueUnboxer;
6056
private final ExecutionInput executionInput;
6157
private final Supplier<ExecutableNormalizedOperation> queryTree;
58+
private final boolean propagateErrors;
6259

6360
// this is modified after creation so it needs to be volatile to ensure visibility across Threads
6461
private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP;
@@ -88,6 +85,7 @@ public class ExecutionContext {
8885
this.executionInput = builder.executionInput;
8986
this.dataLoaderDispatcherStrategy = builder.dataLoaderDispatcherStrategy;
9087
this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables));
88+
this.propagateErrors = builder.propagateErrors;
9189
}
9290

9391

@@ -170,6 +168,9 @@ public ValueUnboxer getValueUnboxer() {
170168
return valueUnboxer;
171169
}
172170

171+
@ExperimentalApi
172+
public boolean propagateErrors() { return propagateErrors; }
173+
173174
/**
174175
* @return true if the current operation is a Query
175176
*/

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22

33
import com.google.common.collect.ImmutableList;
44
import com.google.common.collect.ImmutableMap;
5-
import graphql.ExecutionInput;
6-
import graphql.GraphQLContext;
7-
import graphql.GraphQLError;
8-
import graphql.Internal;
9-
import graphql.PublicApi;
5+
import graphql.*;
106
import graphql.collect.ImmutableKit;
117
import graphql.execution.instrumentation.Instrumentation;
128
import graphql.execution.instrumentation.InstrumentationState;
@@ -46,6 +42,7 @@ public class ExecutionContextBuilder {
4642
Object localContext;
4743
ExecutionInput executionInput;
4844
DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP;
45+
boolean propagateErrors = true;
4946

5047
/**
5148
* @return a new builder of {@link graphql.execution.ExecutionContext}s
@@ -92,6 +89,7 @@ public ExecutionContextBuilder() {
9289
valueUnboxer = other.getValueUnboxer();
9390
executionInput = other.getExecutionInput();
9491
dataLoaderDispatcherStrategy = other.getDataLoaderDispatcherStrategy();
92+
propagateErrors = other.propagateErrors();
9593
}
9694

9795
public ExecutionContextBuilder instrumentation(Instrumentation instrumentation) {
@@ -216,6 +214,13 @@ public ExecutionContextBuilder resetErrors() {
216214
return this;
217215
}
218216

217+
@ExperimentalApi
218+
public ExecutionContextBuilder propagateErrors(boolean propagateErrors) {
219+
this.propagateErrors = propagateErrors;
220+
return this;
221+
}
222+
223+
219224
public ExecutionContext build() {
220225
// preconditions
221226
assertNotNull(executionId, () -> "You must provide a query identifier");

src/main/java/graphql/execution/NonNullableFieldValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public NonNullableFieldValidator(ExecutionContext executionContext, ExecutionSte
3434
*/
3535
public <T> T validate(ExecutionStrategyParameters parameters, T result) throws NonNullableFieldWasNullException {
3636
if (result == null) {
37-
if (executionStepInfo.isNonNullType()) {
37+
if (executionStepInfo.isNonNullType() && executionContext.propagateErrors()) {
3838
// see https://spec.graphql.org/October2021/#sec-Errors-and-Non-Nullability
3939
//
4040
// > If the field returns null because of an error which has already been added to the "errors" list in the response,

src/main/java/graphql/schema/idl/SchemaGenerator.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ public SchemaGenerator() {
5959
* Created a schema from the SDL that is has a mocked runtime.
6060
*
6161
* @param sdl the SDL to be mocked
62-
*
6362
* @return a schema with a mocked runtime
64-
*
6563
* @see RuntimeWiring#MOCKED_WIRING
6664
*/
6765
public static GraphQLSchema createdMockedSchema(String sdl) {
@@ -75,9 +73,7 @@ public static GraphQLSchema createdMockedSchema(String sdl) {
7573
*
7674
* @param typeRegistry this can be obtained via {@link SchemaParser#parse(String)}
7775
* @param wiring this can be built using {@link RuntimeWiring#newRuntimeWiring()}
78-
*
7976
* @return an executable schema
80-
*
8177
* @throws SchemaProblem if there are problems in assembling a schema such as missing type resolvers or no operations defined
8278
*/
8379
public GraphQLSchema makeExecutableSchema(TypeDefinitionRegistry typeRegistry, RuntimeWiring wiring) throws SchemaProblem {
@@ -91,17 +87,15 @@ public GraphQLSchema makeExecutableSchema(TypeDefinitionRegistry typeRegistry, R
9187
* @param options the controlling options
9288
* @param typeRegistry this can be obtained via {@link SchemaParser#parse(String)}
9389
* @param wiring this can be built using {@link RuntimeWiring#newRuntimeWiring()}
94-
*
9590
* @return an executable schema
96-
*
9791
* @throws SchemaProblem if there are problems in assembling a schema such as missing type resolvers or no operations defined
9892
*/
9993
public GraphQLSchema makeExecutableSchema(Options options, TypeDefinitionRegistry typeRegistry, RuntimeWiring wiring) throws SchemaProblem {
10094

10195
TypeDefinitionRegistry typeRegistryCopy = new TypeDefinitionRegistry();
10296
typeRegistryCopy.merge(typeRegistry);
10397

104-
schemaGeneratorHelper.addDirectivesIncludedByDefault(typeRegistryCopy);
98+
schemaGeneratorHelper.addDirectivesIncludedByDefault(typeRegistryCopy, options.addOnErrorDirective);
10599

106100
List<GraphQLError> errors = typeChecker.checkTypeRegistry(typeRegistryCopy, wiring);
107101
if (!errors.isEmpty()) {
@@ -164,11 +158,13 @@ public static class Options {
164158
private final boolean useCommentsAsDescription;
165159
private final boolean captureAstDefinitions;
166160
private final boolean useAppliedDirectivesOnly;
161+
private final boolean addOnErrorDirective;
167162

168-
Options(boolean useCommentsAsDescription, boolean captureAstDefinitions, boolean useAppliedDirectivesOnly) {
163+
Options(boolean useCommentsAsDescription, boolean captureAstDefinitions, boolean useAppliedDirectivesOnly, boolean addOnErrorDirective) {
169164
this.useCommentsAsDescription = useCommentsAsDescription;
170165
this.captureAstDefinitions = captureAstDefinitions;
171166
this.useAppliedDirectivesOnly = useAppliedDirectivesOnly;
167+
this.addOnErrorDirective = addOnErrorDirective;
172168
}
173169

174170
public boolean isUseCommentsAsDescription() {
@@ -183,8 +179,12 @@ public boolean isUseAppliedDirectivesOnly() {
183179
return useAppliedDirectivesOnly;
184180
}
185181

182+
public boolean getAddOnErrorDirective() {
183+
return addOnErrorDirective;
184+
}
185+
186186
public static Options defaultOptions() {
187-
return new Options(true, true, false);
187+
return new Options(true, true, false, false);
188188
}
189189

190190
/**
@@ -193,23 +193,21 @@ public static Options defaultOptions() {
193193
* descriptions are the sanctioned way to make scheme element descriptions.
194194
*
195195
* @param useCommentsAsDescription the flag to control whether comments can be used as schema element descriptions
196-
*
197196
* @return a new Options object
198197
*/
199198
public Options useCommentsAsDescriptions(boolean useCommentsAsDescription) {
200-
return new Options(useCommentsAsDescription, captureAstDefinitions, useAppliedDirectivesOnly);
199+
return new Options(useCommentsAsDescription, captureAstDefinitions, useAppliedDirectivesOnly, addOnErrorDirective);
201200
}
202201

203202
/**
204203
* Memory can be saved if the original AST definitions are not associated with the built runtime types. However
205204
* some tooling may require them.
206205
*
207206
* @param captureAstDefinitions the flag on whether to capture AST definitions
208-
*
209207
* @return a new Options object
210208
*/
211209
public Options captureAstDefinitions(boolean captureAstDefinitions) {
212-
return new Options(useCommentsAsDescription, captureAstDefinitions, useAppliedDirectivesOnly);
210+
return new Options(useCommentsAsDescription, captureAstDefinitions, useAppliedDirectivesOnly, addOnErrorDirective);
213211
}
214212

215213
/**
@@ -218,11 +216,14 @@ public Options captureAstDefinitions(boolean captureAstDefinitions) {
218216
* elements. This flag allows you to only use {@link graphql.schema.GraphQLAppliedDirective} on schema elements.
219217
*
220218
* @param useAppliedDirectivesOnly the flag on whether to use {@link graphql.schema.GraphQLAppliedDirective}s only on schema elements
221-
*
222219
* @return a new Options object
223220
*/
224221
public Options useAppliedDirectivesOnly(boolean useAppliedDirectivesOnly) {
225-
return new Options(useCommentsAsDescription, captureAstDefinitions, useAppliedDirectivesOnly);
222+
return new Options(useCommentsAsDescription, captureAstDefinitions, useAppliedDirectivesOnly, addOnErrorDirective);
223+
}
224+
225+
public Options addOnErrorDirective(boolean addOnErrorDirective) {
226+
return new Options(useCommentsAsDescription, captureAstDefinitions, useAppliedDirectivesOnly, addOnErrorDirective);
226227
}
227228
}
228229
}

0 commit comments

Comments
 (0)