diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml index 16b6e15c61..61be8d20da 100644 --- a/.github/workflows/invoke_test_runner.yml +++ b/.github/workflows/invoke_test_runner.yml @@ -50,7 +50,7 @@ jobs: - id: 'auth' name: 'Authenticate to Google Cloud' - uses: google-github-actions/auth@v2.1.2 + uses: google-github-actions/auth@v2.1.3 with: credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5e90decd21..ef52a4f3cb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -7,10 +7,10 @@ on: pull_request: branches: - master + - 22.x + - 21.x - 20.x - 19.x - - 18.x - - 17.x jobs: buildAndTest: runs-on: ubuntu-latest diff --git a/README.md b/README.md index e065da305d..893dd2f6f7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This is a [GraphQL](https://github.com/graphql/graphql-spec) Java implementation Latest build in Maven central: https://repo1.maven.org/maven2/com/graphql-java/graphql-java/ [![Build](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml/badge.svg)](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml) -[![Latest Release](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?versionPrefix=21.)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) +[![Latest Release](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?versionPrefix=22.)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) [![Latest Snapshot](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?label=maven-central%20snapshot&versionPrefix=0)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) [![MIT licensed](https://img.shields.io/badge/license-MIT-green)](https://github.com/graphql-java/graphql-java/blob/master/LICENSE.md) diff --git a/agent-test/build.gradle b/agent-test/build.gradle index 280aed1bac..2ce1e1892c 100644 --- a/agent-test/build.gradle +++ b/agent-test/build.gradle @@ -4,7 +4,7 @@ plugins { dependencies { implementation(rootProject) - implementation("net.bytebuddy:byte-buddy-agent:1.14.13") + implementation("net.bytebuddy:byte-buddy-agent:1.14.15") testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/agent/build.gradle b/agent/build.gradle index 1cc23ecb3c..c81c0e4d7c 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -6,7 +6,7 @@ plugins { } dependencies { - implementation("net.bytebuddy:byte-buddy:1.14.13") + implementation("net.bytebuddy:byte-buddy:1.14.15") // graphql-java itself implementation(rootProject) } diff --git a/build.gradle b/build.gradle index 3750da10cb..b3fdb18952 100644 --- a/build.gradle +++ b/build.gradle @@ -109,16 +109,16 @@ dependencies { testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0' testImplementation 'org.codehaus.groovy:groovy:3.0.21' testImplementation 'org.codehaus.groovy:groovy-json:3.0.21' - testImplementation 'com.google.code.gson:gson:2.10.1' - testImplementation 'org.eclipse.jetty:jetty-server:11.0.20' - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' + testImplementation 'com.google.code.gson:gson:2.11.0' + testImplementation 'org.eclipse.jetty:jetty-server:11.0.21' + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' testImplementation 'org.awaitility:awaitility-groovy:4.2.0' testImplementation 'com.github.javafaker:javafaker:1.0.2' testImplementation 'org.reactivestreams:reactive-streams-tck:' + reactiveStreamsVersion testImplementation "io.reactivex.rxjava2:rxjava:2.2.21" - testImplementation 'org.testng:testng:7.10.1' // use for reactive streams test inheritance + testImplementation 'org.testng:testng:7.10.2' // use for reactive streams test inheritance testImplementation 'org.openjdk.jmh:jmh-core:1.37' testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' diff --git a/src/main/java/graphql/GraphQLContext.java b/src/main/java/graphql/GraphQLContext.java index 8b913919d3..48f79ce774 100644 --- a/src/main/java/graphql/GraphQLContext.java +++ b/src/main/java/graphql/GraphQLContext.java @@ -96,6 +96,33 @@ public Optional getOrEmpty(Object key) { return Optional.ofNullable(t); } + /** + * This returns true if the value at the specified key is equal to + * {@link Boolean#TRUE} + * + * @param key the key to look up + * + * @return true if the value is equal to {@link Boolean#TRUE} + */ + public boolean getBoolean(Object key) { + Object val = map.get(assertNotNull(key)); + return Boolean.TRUE.equals(val); + } + + /** + * This returns true if the value at the specified key is equal to + * {@link Boolean#TRUE} or the default value if the key is missing + * + * @param key the key to look up + * @param defaultValue the value to use if the key is not present + * + * @return true if the value is equal to {@link Boolean#TRUE} + */ + public boolean getBoolean(Object key, Boolean defaultValue) { + Object val = map.getOrDefault(assertNotNull(key), defaultValue); + return Boolean.TRUE.equals(val); + } + /** * Returns true if the context contains a value for that key * @@ -177,11 +204,11 @@ public GraphQLContext putAll(Consumer contextBuilderCons * Attempts to compute a mapping for the specified key and its * current mapped value (or null if there is no current mapping). * - * @param key key with which the specified value is to be associated + * @param key key with which the specified value is to be associated * @param remappingFunction the function to compute a value + * @param for two * * @return the new value associated with the specified key, or null if none - * @param for two */ public T compute(Object key, BiFunction remappingFunction) { assertNotNull(remappingFunction); @@ -192,11 +219,11 @@ public T compute(Object key, BiFunction rema * If the specified key is not already associated with a value (or is mapped to null), * attempts to compute its value using the given mapping function and enters it into this map unless null. * - * @param key key with which the specified value is to be associated + * @param key key with which the specified value is to be associated * @param mappingFunction the function to compute a value + * @param for two * * @return the current (existing or computed) value associated with the specified key, or null if the computed value is null - * @param for two */ public T computeIfAbsent(Object key, Function mappingFunction) { @@ -207,11 +234,11 @@ public T computeIfAbsent(Object key, Function mappingFu * If the value for the specified key is present and non-null, * attempts to compute a new mapping given the key and its current mapped value. * - * @param key key with which the specified value is to be associated + * @param key key with which the specified value is to be associated * @param remappingFunction the function to compute a value + * @param for two * * @return the new value associated with the specified key, or null if none - * @param for two */ public T computeIfPresent(Object key, BiFunction remappingFunction) { diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 584449553a..a3fff5a8e8 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -154,7 +154,7 @@ private CompletableFuture executeOperation(ExecutionContext exe collectorParameters, operationDefinition.getSelectionSet(), Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) + .map(graphqlContext -> graphqlContext.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) .orElse(false) ); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5e749360a5..de1ae0e178 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -17,6 +17,7 @@ import graphql.execution.directives.QueryDirectivesImpl; import graphql.execution.incremental.DeferredExecutionSupport; import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext; +import graphql.execution.instrumentation.FieldFetchingInstrumentationContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; @@ -298,7 +299,7 @@ DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executi MergedSelectionSet fields = parameters.getFields(); return Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) + .map(graphqlContext -> graphqlContext.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) .orElse(false) ? new DeferredExecutionSupport.DeferredExecutionSupportImpl( fields, @@ -487,7 +488,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, dataFetcher instanceof TrivialDataFetcher); - InstrumentationContext fetchCtx = nonNullCtx(instrumentation.beginFieldFetch(instrumentationFieldFetchParams, + FieldFetchingInstrumentationContext fetchCtx = FieldFetchingInstrumentationContext.nonNullCtx(instrumentation.beginFieldFetching(instrumentationFieldFetchParams, executionContext.getInstrumentationState()) ); @@ -496,6 +497,7 @@ Async.CombinedBuilder getAsyncFieldValueInfo( Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); executionContext.getDataLoaderDispatcherStrategy().fieldFetched(executionContext, parameters, dataFetcher, fetchedObject); fetchCtx.onDispatched(); + fetchCtx.onFetchedValue(fetchedObject); if (fetchedObject instanceof CompletableFuture) { @SuppressWarnings("unchecked") CompletableFuture fetchedValue = (CompletableFuture) fetchedObject; @@ -948,7 +950,7 @@ protected void handleValueException(CompletableFuture overallResult, Thro collectorParameters, parameters.getField(), Optional.ofNullable(executionContext.getGraphQLContext()) - .map(graphqlContext -> (Boolean) graphqlContext.get(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) + .map(graphqlContext -> graphqlContext.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) .orElse(false) ); diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 6decb929cd..f0577aeab0 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -1,7 +1,6 @@ package graphql.execution.instrumentation; import com.google.common.collect.ImmutableList; -import graphql.Assert; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.ExperimentalApi; @@ -159,7 +158,7 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrument } BiFunction mapper = (instrumentation, specificState) -> instrumentation.beginExecuteObject(parameters, specificState); ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; - if (instrumentations.size() == 1) { + if (instrumentations.size() == 1) { return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0)); } return new ChainedExecuteObjectInstrumentationContext(chainedMapAndDropNulls(chainedInstrumentationState, mapper)); @@ -182,11 +181,26 @@ public InstrumentationContext beginSubscribedFieldEvent(Instrum return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldExecution(parameters, specificState)); } + @SuppressWarnings("deprecation") @Override public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState)); } + @Override + public FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return FieldFetchingInstrumentationContext.NOOP; + } + BiFunction mapper = (instrumentation, specificState) -> instrumentation.beginFieldFetching(parameters, specificState); + ChainedInstrumentationState chainedInstrumentationState = (ChainedInstrumentationState) state; + if (instrumentations.size() == 1) { + return mapper.apply(instrumentations.get(0), chainedInstrumentationState.getState(0)); + } + ImmutableList objects = chainedMapAndDropNulls(chainedInstrumentationState, mapper); + return new ChainedFieldFetchingInstrumentationContext(objects); + } + @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return chainedCtx(state, (instrumentation, specificState) -> instrumentation.beginFieldCompletion(parameters, specificState)); @@ -344,8 +358,33 @@ public void onFieldValuesException() { } } + private static class ChainedFieldFetchingInstrumentationContext implements FieldFetchingInstrumentationContext { + + private final ImmutableList contexts; + + ChainedFieldFetchingInstrumentationContext(ImmutableList contexts) { + this.contexts = contexts; + } + + @Override + public void onDispatched() { + contexts.forEach(FieldFetchingInstrumentationContext::onDispatched); + } + + @Override + public void onFetchedValue(Object fetchedValue) { + contexts.forEach(context -> context.onFetchedValue(fetchedValue)); + } + + @Override + public void onCompleted(Object result, Throwable t) { + contexts.forEach(context -> context.onCompleted(result, t)); + } + } + private static class ChainedDeferredExecutionStrategyInstrumentationContext implements InstrumentationContext { + private final List> contexts; ChainedDeferredExecutionStrategyInstrumentationContext(List> contexts) { diff --git a/src/main/java/graphql/execution/instrumentation/FieldFetchingInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/FieldFetchingInstrumentationContext.java new file mode 100644 index 0000000000..d8e51269be --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/FieldFetchingInstrumentationContext.java @@ -0,0 +1,68 @@ +package graphql.execution.instrumentation; + +import graphql.Internal; +import graphql.PublicSpi; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@PublicSpi +public interface FieldFetchingInstrumentationContext extends InstrumentationContext { + + @Internal + FieldFetchingInstrumentationContext NOOP = new FieldFetchingInstrumentationContext() { + @Override + public void onDispatched() { + } + + @Override + public void onCompleted(Object result, Throwable t) { + } + + @Override + public void onFetchedValue(Object fetchedValue) { + } + }; + + /** + * This creates a no-op {@link InstrumentationContext} if the one pass in is null + * + * @param nullableContext a {@link InstrumentationContext} that can be null + * + * @return a non null {@link InstrumentationContext} that maybe a no-op + */ + @NotNull + @Internal + static FieldFetchingInstrumentationContext nonNullCtx(FieldFetchingInstrumentationContext nullableContext) { + return nullableContext == null ? NOOP : nullableContext; + } + + @Internal + static FieldFetchingInstrumentationContext adapter(@Nullable InstrumentationContext context) { + if (context == null) { + return null; + } + return new FieldFetchingInstrumentationContext() { + @Override + public void onDispatched() { + context.onDispatched(); + } + + @Override + public void onCompleted(Object result, Throwable t) { + context.onCompleted(result, t); + } + + @Override + public void onFetchedValue(Object fetchedValue) { + } + }; + } + + /** + * This is called back with value fetched for the field. + * + * @param fetchedValue a value that a field's {@link graphql.schema.DataFetcher} returned + */ + default void onFetchedValue(Object fetchedValue) { + } +} diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 977422e565..11819347ad 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -196,12 +196,31 @@ default InstrumentationContext beginFieldExecution(InstrumentationFieldP * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} * * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + * + * @deprecated use {@link #beginFieldFetching(InstrumentationFieldFetchParameters, InstrumentationState)} instead */ + @Deprecated(since = "2024-04-18") @Nullable default InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return noOp(); } + /** + * This is called just before a field {@link DataFetcher} is invoked. The {@link FieldFetchingInstrumentationContext#onFetchedValue(Object)} + * callback will be invoked once a value is returned by a {@link DataFetcher} but perhaps before + * its value is completed if it's a {@link CompletableFuture} value. + * + * @param parameters the parameters to this step + * @param state the state created during the call to {@link #createStateAsync(InstrumentationCreateStateParameters)} + * + * @return a nullable {@link InstrumentationContext} object that will be called back when the step ends (assuming it's not null) + */ + @Nullable + default FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + InstrumentationContext ctx = beginFieldFetch(parameters, state); + return FieldFetchingInstrumentationContext.adapter(ctx); + } + /** * This is called just before the complete field is started. * diff --git a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java index 1928df84f0..8d8d3cd956 100644 --- a/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/NoContextChainedInstrumentation.java @@ -98,6 +98,11 @@ public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchP return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldFetch(parameters, specificState)); } + @Override + public FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldFetching(parameters, specificState)); + } + @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return runAll(state, (instrumentation, specificState) -> instrumentation.beginFieldCompletion(parameters, specificState)); diff --git a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java index dfffa5b729..b46cedf9c7 100644 --- a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java @@ -102,7 +102,6 @@ public class SimplePerformantInstrumentation implements Instrumentation { return noOp(); } - @Override public @Nullable InstrumentationContext beginFieldCompletion(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { return noOp(); diff --git a/src/main/java/graphql/incremental/DeferPayload.java b/src/main/java/graphql/incremental/DeferPayload.java index 58e5ac0994..8d6047de3a 100644 --- a/src/main/java/graphql/incremental/DeferPayload.java +++ b/src/main/java/graphql/incremental/DeferPayload.java @@ -22,12 +22,12 @@ private DeferPayload(Object data, List path, String label, List the type to cast the result to + * @return the resolved data */ @Nullable public T getData() { - //noinspection unchecked + // noinspection unchecked return (T) this.data; } @@ -37,11 +37,7 @@ public T getData() { @Override public Map toSpecification() { Map map = new LinkedHashMap<>(super.toSpecification()); - - if (data != null) { - map.put("data", data); - } - + map.put("data", data); return map; } diff --git a/src/main/java/graphql/incremental/StreamPayload.java b/src/main/java/graphql/incremental/StreamPayload.java index e8bdfcf85c..88e1e7b543 100644 --- a/src/main/java/graphql/incremental/StreamPayload.java +++ b/src/main/java/graphql/incremental/StreamPayload.java @@ -21,12 +21,12 @@ private StreamPayload(List items, List path, String label, List< } /** - * @return the resolved list of items * @param the type to cast the result to + * @return the resolved list of items */ @Nullable public List getItems() { - //noinspection unchecked + // noinspection unchecked return (List) this.items; } @@ -36,11 +36,7 @@ public List getItems() { @Override public Map toSpecification() { Map map = new LinkedHashMap<>(super.toSpecification()); - - if (items != null) { - map.put("items", items); - } - + map.put("items", items); return map; } diff --git a/src/main/java/graphql/introspection/GoodFaithIntrospection.java b/src/main/java/graphql/introspection/GoodFaithIntrospection.java index bd7285cbd1..ae7da12569 100644 --- a/src/main/java/graphql/introspection/GoodFaithIntrospection.java +++ b/src/main/java/graphql/introspection/GoodFaithIntrospection.java @@ -134,7 +134,7 @@ private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { if (!isEnabledJvmWide()) { return false; } - return !graphQlContext.getOrDefault(GOOD_FAITH_INTROSPECTION_DISABLED, false); + return !graphQlContext.getBoolean(GOOD_FAITH_INTROSPECTION_DISABLED, false); } public static class BadFaithIntrospectionError implements GraphQLError { diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index ced6cbf818..4aea32bda4 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -144,7 +144,7 @@ private static boolean isIntrospectionEnabled(GraphQLContext graphQlContext) { if (!isEnabledJvmWide()) { return false; } - return !graphQlContext.getOrDefault(INTROSPECTION_DISABLED, false); + return !graphQlContext.getBoolean(INTROSPECTION_DISABLED, false); } private static final Map> introspectionDataFetchers = new LinkedHashMap<>(); diff --git a/src/main/java/graphql/schema/DataFetchingEnvironment.java b/src/main/java/graphql/schema/DataFetchingEnvironment.java index ce46ad75c9..3c19f03329 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironment.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironment.java @@ -13,6 +13,8 @@ import graphql.language.OperationDefinition; import org.dataloader.DataLoader; import org.dataloader.DataLoaderRegistry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Locale; @@ -37,6 +39,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * * @return can be null for the root query, otherwise it is never null */ + @Nullable T getSource(); /** @@ -61,6 +64,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * * @return the named argument or null if it's not present */ + @Nullable T getArgument(String name); /** @@ -97,6 +101,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * * @return can NOT be null */ + @NotNull GraphQLContext getGraphQlContext(); /** @@ -114,6 +119,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * * @return can be null if no field context objects are passed back by previous parent fields */ + @Nullable T getLocalContext(); /** @@ -228,6 +234,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * * @see org.dataloader.DataLoaderRegistry#getDataLoader(String) */ + @Nullable DataLoader getDataLoader(String dataLoaderName); /** diff --git a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java index 4377330e48..440ec86159 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java @@ -17,6 +17,8 @@ import graphql.language.OperationDefinition; import org.dataloader.DataLoader; import org.dataloader.DataLoaderRegistry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Locale; @@ -129,12 +131,12 @@ public T getContext() { } @Override - public GraphQLContext getGraphQlContext() { + public @NotNull GraphQLContext getGraphQlContext() { return graphQLContext; } @Override - public T getLocalContext() { + public @Nullable T getLocalContext() { return (T) localContext; } @@ -204,7 +206,7 @@ public ExecutionStepInfo getExecutionStepInfo() { } @Override - public DataLoader getDataLoader(String dataLoaderName) { + public @Nullable DataLoader getDataLoader(String dataLoaderName) { return dataLoaderRegistry.getDataLoader(dataLoaderName); } diff --git a/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java b/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java index 41d6795556..0509684647 100644 --- a/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java +++ b/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java @@ -12,6 +12,8 @@ import graphql.language.OperationDefinition; import org.dataloader.DataLoader; import org.dataloader.DataLoaderRegistry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Locale; @@ -70,12 +72,12 @@ public T getContext() { } @Override - public GraphQLContext getGraphQlContext() { + public @NotNull GraphQLContext getGraphQlContext() { return delegateEnvironment.getGraphQlContext(); } @Override - public T getLocalContext() { + public @Nullable T getLocalContext() { return delegateEnvironment.getLocalContext(); } @@ -146,7 +148,7 @@ public QueryDirectives getQueryDirectives() { } @Override - public DataLoader getDataLoader(String dataLoaderName) { + public @Nullable DataLoader getDataLoader(String dataLoaderName) { return delegateEnvironment.getDataLoader(dataLoaderName); } diff --git a/src/main/java/graphql/schema/idl/RuntimeWiring.java b/src/main/java/graphql/schema/idl/RuntimeWiring.java index 0a95adce08..941ee9534c 100644 --- a/src/main/java/graphql/schema/idl/RuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/RuntimeWiring.java @@ -116,10 +116,31 @@ public Map> getDataFetchers() { return dataFetchers; } + /** + * This is deprecated because the name has the wrong plural case. + * + * @param typeName the type for fetch a map of per field data fetchers for + * + * @return a map of field data fetchers for a type + * + * @deprecated See {@link #getDataFetchersForType(String)} + */ + @Deprecated(since = "2024-04-28") public Map getDataFetcherForType(String typeName) { return dataFetchers.computeIfAbsent(typeName, k -> new LinkedHashMap<>()); } + /** + * This returns a map of the data fetchers per field on that named type. + * + * @param typeName the type for fetch a map of per field data fetchers for + * + * @return a map of field data fetchers for a type + */ + public Map getDataFetchersForType(String typeName) { + return dataFetchers.computeIfAbsent(typeName, k -> new LinkedHashMap<>()); + } + public DataFetcher getDefaultDataFetcherForType(String typeName) { return defaultDataFetchers.get(typeName); } @@ -284,7 +305,10 @@ public Builder type(TypeRuntimeWiring typeRuntimeWiring) { } typeDataFetchers.putAll(typeRuntimeWiring.getFieldDataFetchers()); - defaultDataFetchers.put(typeName, typeRuntimeWiring.getDefaultDataFetcher()); + DataFetcher defaultDataFetcher = typeRuntimeWiring.getDefaultDataFetcher(); + if (defaultDataFetcher != null) { + defaultDataFetchers.put(typeName, defaultDataFetcher); + } TypeResolver typeResolver = typeRuntimeWiring.getTypeResolver(); if (typeResolver != null) { diff --git a/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java b/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java index aa4878efb4..0b4e74c45b 100644 --- a/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java +++ b/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java @@ -839,7 +839,7 @@ private DataFetcherFactory buildDataFetcherFactory(BuildContext buildCtx, dataFetcher = wiringFactory.getDataFetcher(wiringEnvironment); assertNotNull(dataFetcher, () -> "The WiringFactory indicated it provides a data fetcher but then returned null"); } else { - dataFetcher = runtimeWiring.getDataFetcherForType(parentTypeName).get(fieldName); + dataFetcher = runtimeWiring.getDataFetchersForType(parentTypeName).get(fieldName); if (dataFetcher == null) { dataFetcher = runtimeWiring.getDefaultDataFetcherForType(parentTypeName); if (dataFetcher == null) { diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index 9ac97f6a2e..c51e4a78e4 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -19,6 +19,7 @@ import graphql.language.ObjectTypeDefinition; import graphql.language.ScalarTypeDefinition; import graphql.language.SchemaDefinition; +import graphql.language.SchemaExtensionDefinition; import graphql.language.TypeDefinition; import graphql.language.UnionTypeDefinition; import graphql.schema.DefaultGraphqlTypeComparatorRegistry; @@ -283,7 +284,7 @@ public Options includeDirectiveDefinitions(boolean flag) { /** * This is a Predicate that decides whether a directive definition is printed. * - * @param includeDirectiveDefinition the predicate to decide of a directive defintion is printed + * @param includeDirectiveDefinition the predicate to decide of a directive definition is printed * * @return new instance of options */ @@ -482,7 +483,7 @@ public SchemaPrinter(Options options) { /** * This can print an in memory GraphQL IDL document back to a logical schema definition. - * If you want to turn a Introspection query result into a Document (and then into a printed + * If you want to turn an Introspection query result into a Document (and then into a printed * schema) then use {@link graphql.introspection.IntrospectionResultToSchema#createSchemaDefinition(java.util.Map)} * first to get the {@link graphql.language.Document} and then print that. * @@ -773,6 +774,17 @@ private boolean shouldPrintAsAst(TypeDefinition definition) { return options.isUseAstDefinitions() && definition != null; } + /** + * This will return true if the options say to use the AST and we have an AST element + * + * @param definition the AST schema definition + * + * @return true if we should print using AST nodes + */ + private boolean shouldPrintAsAst(SchemaDefinition definition) { + return options.isUseAstDefinitions() && definition != null; + } + /** * This will print out a runtime graphql schema element using its contained AST type definition. This * must be guarded by a called to {@link #shouldPrintAsAst(TypeDefinition)} @@ -792,6 +804,25 @@ private void printAsAst(PrintWriter out, TypeDefinition definition, List extensions) { + out.printf("%s\n", AstPrinter.printAst(definition)); + if (extensions != null) { + for (SchemaExtensionDefinition extension : extensions) { + out.printf("\n%s\n", AstPrinter.printAst(extension)); + } + } + out.print('\n'); + } + + private static String printAst(InputValueWithState value, GraphQLInputType type) { return AstPrinter.printAst(ValuesResolver.valueToLiteral(value, type, GraphQLContext.getDefault(), Locale.getDefault())); } @@ -803,7 +834,7 @@ private SchemaElementPrinter schemaPrinter() { GraphQLObjectType subscriptionType = schema.getSubscriptionType(); // when serializing a GraphQL schema using the type system language, a - // schema definition should be omitted if only uses the default root type names. + // schema definition should be omitted only if it uses the default root type names. boolean needsSchemaPrinted = options.isIncludeSchemaDefinition(); if (!needsSchemaPrinted) { @@ -819,21 +850,25 @@ private SchemaElementPrinter schemaPrinter() { } if (needsSchemaPrinted) { - if (hasAstDefinitionComments(schema) || hasDescription(schema)) { - out.print(printComments(schema, "")); - } - List directives = DirectivesUtil.toAppliedDirectives(schema.getSchemaAppliedDirectives(), schema.getSchemaDirectives()); - out.format("schema %s{\n", directivesString(GraphQLSchemaElement.class, directives)); - if (queryType != null) { - out.format(" query: %s\n", queryType.getName()); - } - if (mutationType != null) { - out.format(" mutation: %s\n", mutationType.getName()); - } - if (subscriptionType != null) { - out.format(" subscription: %s\n", subscriptionType.getName()); + if (shouldPrintAsAst(schema.getDefinition())) { + printAsAst(out, schema.getDefinition(), schema.getExtensionDefinitions()); + } else { + if (hasAstDefinitionComments(schema) || hasDescription(schema)) { + out.print(printComments(schema, "")); + } + List directives = DirectivesUtil.toAppliedDirectives(schema.getSchemaAppliedDirectives(), schema.getSchemaDirectives()); + out.format("schema %s{\n", directivesString(GraphQLSchemaElement.class, directives)); + if (queryType != null) { + out.format(" query: %s\n", queryType.getName()); + } + if (mutationType != null) { + out.format(" mutation: %s\n", mutationType.getName()); + } + if (subscriptionType != null) { + out.format(" subscription: %s\n", subscriptionType.getName()); + } + out.format("}\n\n"); } - out.format("}\n\n"); } }; } diff --git a/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java b/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java index 3480a5d6b0..5e2e333cef 100644 --- a/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/TypeRuntimeWiring.java @@ -184,6 +184,9 @@ private void assertFieldStrictly(String fieldName) { */ public Builder defaultDataFetcher(DataFetcher dataFetcher) { assertNotNull(dataFetcher); + if (strictMode && defaultDataFetcher != null) { + throw new StrictModeWiringException(format("The type %s has already has a default data fetcher defined", typeName)); + } defaultDataFetcher = dataFetcher; return this; } diff --git a/src/main/java/graphql/schema/validation/DeprecatedInputObjectAndArgumentsAreValid.java b/src/main/java/graphql/schema/validation/DeprecatedInputObjectAndArgumentsAreValid.java new file mode 100644 index 0000000000..ce619223cc --- /dev/null +++ b/src/main/java/graphql/schema/validation/DeprecatedInputObjectAndArgumentsAreValid.java @@ -0,0 +1,64 @@ +package graphql.schema.validation; + +import graphql.Directives; +import graphql.Internal; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeUtil; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; + +import static java.lang.String.format; + +/* + From the spec: + The @deprecated directive must not appear on required (non-null without a default) arguments + or input object field definitions. + */ +@Internal +public class DeprecatedInputObjectAndArgumentsAreValid extends GraphQLTypeVisitorStub { + + @Override + public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext context) { + // There can only be at most one @deprecated, because it is not a repeatable directive + GraphQLAppliedDirective deprecatedDirective = inputObjectField.getAppliedDirective(Directives.DEPRECATED_DIRECTIVE_DEFINITION.getName()); + + if (deprecatedDirective != null && GraphQLTypeUtil.isNonNull(inputObjectField.getType()) && !inputObjectField.hasSetDefaultValue()) { + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode(); + SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); + String message = format("Required input field '%s.%s' cannot be deprecated.", inputObjectType.getName(), inputObjectField.getName()); + errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.RequiredInputFieldCannotBeDeprecated, message)); + } + return TraversalControl.CONTINUE; + } + + // An argument can appear as either a field argument or a directive argument. This visitor will visit both field arguments and directive arguments. + // An applied directive's argument cannot be deprecated. + @Override + public TraversalControl visitGraphQLArgument(GraphQLArgument argument, TraverserContext context) { + // There can only be at most one @deprecated, because it is not a repeatable directive + GraphQLAppliedDirective deprecatedDirective = argument.getAppliedDirective(Directives.DEPRECATED_DIRECTIVE_DEFINITION.getName()); + + if (deprecatedDirective != null && GraphQLTypeUtil.isNonNull(argument.getType()) && !argument.hasSetDefaultValue()) { + if (context.getParentNode() instanceof GraphQLFieldDefinition) { + GraphQLFieldDefinition fieldDefinition = (GraphQLFieldDefinition) context.getParentNode(); + SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); + String message = format("Required argument '%s' on field '%s' cannot be deprecated.", argument.getName(), fieldDefinition.getName()); + errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.RequiredFieldArgumentCannotBeDeprecated, message)); + } else if (context.getParentNode() instanceof GraphQLDirective) { + GraphQLDirective directive = (GraphQLDirective) context.getParentNode(); + SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); + String message = format("Required argument '%s' on directive '%s' cannot be deprecated.", argument.getName(), directive.getName()); + errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.RequiredDirectiveArgumentCannotBeDeprecated, message)); + } + } + return TraversalControl.CONTINUE; + } + +} diff --git a/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java b/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java index 08209bb6b2..b8392b18d7 100644 --- a/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java +++ b/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java @@ -21,5 +21,8 @@ public enum SchemaValidationErrorType implements SchemaValidationErrorClassifica OutputTypeUsedInInputTypeContext, InputTypeUsedInOutputTypeContext, OneOfDefaultValueOnField, - OneOfNonNullableField + OneOfNonNullableField, + RequiredInputFieldCannotBeDeprecated, + RequiredFieldArgumentCannotBeDeprecated, + RequiredDirectiveArgumentCannotBeDeprecated } diff --git a/src/main/java/graphql/schema/validation/SchemaValidator.java b/src/main/java/graphql/schema/validation/SchemaValidator.java index 0d8d7f271b..1f676fb77e 100644 --- a/src/main/java/graphql/schema/validation/SchemaValidator.java +++ b/src/main/java/graphql/schema/validation/SchemaValidator.java @@ -26,6 +26,7 @@ public SchemaValidator() { rules.add(new AppliedDirectiveArgumentsAreValid()); rules.add(new InputAndOutputTypesUsedAppropriately()); rules.add(new OneOfInputObjectRules()); + rules.add(new DeprecatedInputObjectAndArgumentsAreValid()); } public List getRules() { diff --git a/src/test/groovy/graphql/GraphQLContextTest.groovy b/src/test/groovy/graphql/GraphQLContextTest.groovy index 8eebb17653..d411143db6 100644 --- a/src/test/groovy/graphql/GraphQLContextTest.groovy +++ b/src/test/groovy/graphql/GraphQLContextTest.groovy @@ -276,4 +276,20 @@ class GraphQLContextTest extends Specification { executionResult.errors.isEmpty() executionResult.data == [field: "ctx1value"] } + + def "boolean getters work"() { + when: + def context = GraphQLContext.newContext().of( + "f", false, + "t", true, + "notABool", "true" + ).build() + + then: + !context.getBoolean("f") + context.getBoolean("t") + !context.getBoolean("notABool") + !context.getBoolean("missing") + context.getBoolean("missing", true) + } } diff --git a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy index e7844cc750..11e6e2d1f8 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferExecutionSupportIntegrationTest.groovy @@ -90,9 +90,10 @@ class DeferExecutionSupportIntegrationTest extends Specification { private static DataFetcher resolve(Object value, Integer sleepMs, boolean allowMultipleCalls) { return new DataFetcher() { boolean executed = false + @Override Object get(DataFetchingEnvironment environment) throws Exception { - if(executed && !allowMultipleCalls) { + if (executed && !allowMultipleCalls) { throw new IllegalStateException("This data fetcher can run only once") } executed = true @@ -298,7 +299,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { }) def indexOfId3 = Iterables.indexOf(incrementalResults, { - it.incremental[0] == [path: ["post3"], label:"defer-id3", data: [id3: "3"]] + it.incremental[0] == [path: ["post3"], label: "defer-id3", data: [id3: "3"]] }) // Assert that both post3 and id3 are present @@ -349,7 +350,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { def incrementalResults = getIncrementalResults(initialResult) then: - if(type == "Post") { + if (type == "Post") { assert incrementalResults == [ [ hasNext : false, @@ -481,8 +482,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : false, incremental: [ [ - path : ["post"], - data : [summary: "A summary"] + path: ["post"], + data: [summary: "A summary"] ] ] ] @@ -519,8 +520,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : false, incremental: [ [ - path : ["post"], - data : [resolvesToNull: null] + path: ["post"], + data: [resolvesToNull: null] ] ] ] @@ -560,8 +561,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : false, incremental: [ [ - path : ["post"], - data : [summary: "A summary", text: "The full text"] + path: ["post"], + data: [summary: "A summary", text: "The full text"] ] ] ] @@ -739,8 +740,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { incremental: [ [ label: "summary-defer", - path: ["post"], - data: [summary: "A summary"] + path : ["post"], + data : [summary: "A summary"] ] ] ], @@ -749,8 +750,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { incremental: [ [ label: "text-defer", - path: ["post"], - data: [text: "The full text"] + path : ["post"], + data : [text: "The full text"] ] ] ] @@ -1211,8 +1212,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : false, incremental: [ [ - path : ["post"], - data : [text: "The full text"], + path: ["post"], + data: [text: "The full text"], ] ] ] @@ -1270,8 +1271,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : false, incremental: [ [ - path : ["post"], - data : [text: "The full text"], + path: ["post"], + data: [text: "The full text"], ] ] ] @@ -1329,8 +1330,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : false, incremental: [ [ - path : ["post"], - data : [text: "The full text"], + path: ["post"], + data: [text: "The full text"], ] ] ] @@ -1371,6 +1372,7 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : true, incremental: [ [ + data : null, path : ["post"], errors: [ [ @@ -1388,8 +1390,8 @@ class DeferExecutionSupportIntegrationTest extends Specification { hasNext : false, incremental: [ [ - path : ["post"], - data : [text: "The full text"], + path: ["post"], + data: [text: "The full text"], ] ] ] diff --git a/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy b/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy index 252d6ca449..77da81298a 100644 --- a/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy +++ b/src/test/groovy/graphql/execution/incremental/DeferredCallTest.groovy @@ -69,6 +69,7 @@ class DeferredCallTest extends Specification { then: deferPayload.toSpecification() == [ + data : null, path : ["path"], label : "my-label", errors: [ diff --git a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy index 5d6fbb1d8e..822d08f5a6 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ModernTestingInstrumentation.groovy @@ -86,9 +86,9 @@ class ModernTestingInstrumentation implements Instrumentation { } @Override - InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { assert state == instrumentationState - return new TestingInstrumentContext("fetch-$parameters.field.name", executionList, throwableList, useOnDispatch) + return new TestingFieldFetchingInstrumentationContext("fetch-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override diff --git a/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy b/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy new file mode 100644 index 0000000000..02cbf9d928 --- /dev/null +++ b/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy @@ -0,0 +1,9 @@ +package graphql.execution.instrumentation + +class TestingFieldFetchingInstrumentationContext extends TestingInstrumentContext> implements FieldFetchingInstrumentationContext { + + TestingFieldFetchingInstrumentationContext(Object op, Object executionList, Object throwableList, Boolean useOnDispatch) { + super(op, executionList, throwableList, useOnDispatch) + } +} + diff --git a/src/test/groovy/graphql/incremental/DeferPayloadTest.groovy b/src/test/groovy/graphql/incremental/DeferPayloadTest.groovy new file mode 100644 index 0000000000..90d6a0899c --- /dev/null +++ b/src/test/groovy/graphql/incremental/DeferPayloadTest.groovy @@ -0,0 +1,21 @@ +package graphql.incremental + + +import spock.lang.Specification + +class DeferPayloadTest extends Specification { + def "null data is included"() { + def payload = DeferPayload.newDeferredItem() + .data(null) + .build() + + when: + def spec = payload.toSpecification() + + then: + spec == [ + data : null, + path : null, + ] + } +} diff --git a/src/test/groovy/graphql/incremental/StreamPayloadTest.groovy b/src/test/groovy/graphql/incremental/StreamPayloadTest.groovy new file mode 100644 index 0000000000..bee0d88631 --- /dev/null +++ b/src/test/groovy/graphql/incremental/StreamPayloadTest.groovy @@ -0,0 +1,20 @@ +package graphql.incremental + +import spock.lang.Specification + +class StreamPayloadTest extends Specification { + def "null data is included"() { + def payload = StreamPayload.newStreamedItem() + .items(null) + .build() + + when: + def spec = payload.toSpecification() + + then: + spec == [ + items: null, + path : null, + ] + } +} diff --git a/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy b/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy index 687a0f62ae..29133b21bd 100644 --- a/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy +++ b/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy @@ -1,6 +1,7 @@ package graphql.schema import graphql.GraphQLContext +import org.jetbrains.annotations.NotNull import spock.lang.Specification class DelegatingDataFetchingEnvironmentTest extends Specification { @@ -22,6 +23,7 @@ class DelegatingDataFetchingEnvironmentTest extends Specification { when: def delegatingDFE = new DelegatingDataFetchingEnvironment(dfe) { + @NotNull @Override GraphQLContext getGraphQlContext() { return GraphQLContext.of(["key": "overriddenContext"]) diff --git a/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy b/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy index bd3c225d2a..907d89a1bf 100644 --- a/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy +++ b/src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy @@ -195,6 +195,7 @@ class RuntimeWiringTest extends Specification { def "strict mode can stop certain redefinitions"() { DataFetcher DF1 = env -> "x" + DataFetcher DF2 = env -> "x" TypeResolver TR1 = env -> null EnumValuesProvider EVP1 = name -> null @@ -236,5 +237,45 @@ class RuntimeWiringTest extends Specification { def e4 = thrown(StrictModeWiringException) e4.message == "The scalar String is already defined" + when: + TypeRuntimeWiring.newTypeWiring("Foo") + .strictMode() + .defaultDataFetcher(DF1) + .defaultDataFetcher(DF2) + + then: + def e5 = thrown(StrictModeWiringException) + e5.message == "The type Foo has already has a default data fetcher defined" + } + + def "overwrite default data fetchers if they are null"() { + + DataFetcher DF1 = env -> "x" + DataFetcher DF2 = env -> "x" + DataFetcher DEFAULT_DF = env -> null + DataFetcher DEFAULT_DF2 = env -> null + + when: + def runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF)) + .type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1)) + .type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF2)) + .build() + + then: + runtimeWiring.getDefaultDataFetcherForType("Foo") == DEFAULT_DF + + when: + runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF)) + .type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1)) + .type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF2)) + // we can specifically overwrite it later + .type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF2)) + .build() + + then: + runtimeWiring.getDefaultDataFetcherForType("Foo") == DEFAULT_DF2 + } } diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy index 1cff3fffec..2802f72808 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy @@ -1342,9 +1342,195 @@ type Query { ''' } + def "can print extend schema block when AST printing enabled"() { + def sdl = ''' + directive @schemaDirective on SCHEMA + + """ + My schema block description + """ + schema { + mutation: MyMutation + } + + extend schema @schemaDirective { + query: MyQuery + } + + extend schema { + subscription: MySubscription + } + + type MyQuery { + foo: String + } + + type MyMutation { + pizza: String + } + + type MySubscription { + chippies: String + } + ''' + + when: + def runtimeWiring = newRuntimeWiring().build() + + def options = SchemaGenerator.Options.defaultOptions() + def types = new SchemaParser().parse(sdl) + GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(options, types, runtimeWiring) + + def printOptions = defaultOptions() + .useAstDefinitions(true) + .includeSchemaDefinition(true) + def result = new SchemaPrinter(printOptions).print(schema) + + then: + result == '''""" +My schema block description +""" +schema { + mutation: MyMutation +} + +extend schema @schemaDirective { + query: MyQuery +} + +extend schema { + subscription: MySubscription +} + +"Marks the field, argument, input field or enum value as deprecated" +directive @deprecated( + "The reason for the deprecation" + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + +"Directs the executor to include this field or fragment only when the `if` argument is true" +directive @include( + "Included when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Indicates an Input Object is a OneOf Input Object." +directive @oneOf on INPUT_OBJECT + +directive @schemaDirective on SCHEMA + +"Directs the executor to skip this field or fragment when the `if` argument is true." +directive @skip( + "Skipped when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Exposes a URL that specifies the behaviour of this scalar." +directive @specifiedBy( + "The URL that specifies the behaviour of this scalar." + url: String! + ) on SCALAR + +type MyMutation { + pizza: String +} + +type MyQuery { + foo: String +} + +type MySubscription { + chippies: String +} +''' + } + + def "will not print extend schema block when AST printing not enabled"() { + def sdl = ''' + directive @schemaDirective on SCHEMA + + """ + My schema block description + """ + schema { + mutation: MyMutation + } + + extend schema @schemaDirective { + query: MyQuery + } + + type MyQuery { + foo: String + } + + type MyMutation { + pizza: String + } + ''' + + when: + def runtimeWiring = newRuntimeWiring().build() + + def options = SchemaGenerator.Options.defaultOptions() + def types = new SchemaParser().parse(sdl) + GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(options, types, runtimeWiring) + + def printOptions = defaultOptions() + .useAstDefinitions(false) + .includeSchemaDefinition(true) + def result = new SchemaPrinter(printOptions).print(schema) + + then: + result == '''"My schema block description" +schema @schemaDirective{ + query: MyQuery + mutation: MyMutation +} + +"Marks the field, argument, input field or enum value as deprecated" +directive @deprecated( + "The reason for the deprecation" + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + +"Directs the executor to include this field or fragment only when the `if` argument is true" +directive @include( + "Included when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Indicates an Input Object is a OneOf Input Object." +directive @oneOf on INPUT_OBJECT + +directive @schemaDirective on SCHEMA + +"Directs the executor to skip this field or fragment when the `if` argument is true." +directive @skip( + "Skipped when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Exposes a URL that specifies the behaviour of this scalar." +directive @specifiedBy( + "The URL that specifies the behaviour of this scalar." + url: String! + ) on SCALAR + +type MyMutation { + pizza: String +} + +type MyQuery { + foo: String +} +''' + } + def "can print a schema as AST elements"() { def sdl = ''' directive @directive1 on SCALAR + type Query { foo : String } diff --git a/src/test/groovy/graphql/schema/validation/DeprecatedInputObjectAndArgumentsAreValidTest.groovy b/src/test/groovy/graphql/schema/validation/DeprecatedInputObjectAndArgumentsAreValidTest.groovy new file mode 100644 index 0000000000..056f134db4 --- /dev/null +++ b/src/test/groovy/graphql/schema/validation/DeprecatedInputObjectAndArgumentsAreValidTest.groovy @@ -0,0 +1,296 @@ +package graphql.schema.validation + +import graphql.TestUtil +import spock.lang.Specification + +class DeprecatedInputObjectAndArgumentsAreValidTest extends Specification { + + def "required input field cannot be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(pizzaInfo: PizzaInfo!): String + } + + input PizzaInfo { + name: String! + pineapples: Boolean! @deprecated(reason: "Don't need this input field") + spicy: Boolean @deprecated(reason: "Don't need this nullable input field") + } + ''' + + when: + TestUtil.schema(sdl) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.getErrors().size() == 1 + schemaProblem.getErrors().first().description == "Required input field 'PizzaInfo.pineapples' cannot be deprecated." + } + + def "multiple required input fields cannot be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(pizzaInfo: PizzaInfo!): String + } + + input PizzaInfo { + name: String! + pineapples: Boolean! @deprecated(reason: "Don't need this input field") + spicy: Boolean! @deprecated(reason: "Don't need this input field") + } + ''' + + when: + TestUtil.schema(sdl) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.getErrors().size() == 2 + schemaProblem.getErrors()[0].description == "Required input field 'PizzaInfo.pineapples' cannot be deprecated." + schemaProblem.getErrors()[1].description == "Required input field 'PizzaInfo.spicy' cannot be deprecated." + } + + def "required input field list cannot be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(pizzaInfos: [PizzaInfo]!): String + } + + input PizzaInfo { + name: String! + pineapples: Boolean! @deprecated(reason: "Don't need this input field") + } + ''' + + when: + TestUtil.schema(sdl) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.getErrors().size() == 1 + schemaProblem.getErrors().first().description == "Required input field 'PizzaInfo.pineapples' cannot be deprecated." + } + + def "nullable input field can be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(pizzaInfo: PizzaInfo!): String + } + + input PizzaInfo { + name: String! + pineapples: Boolean @deprecated(reason: "Don't need this input field") + } + ''' + + when: + TestUtil.schema(sdl) + + then: + noExceptionThrown() + } + + def "non-nullable input field with default value can be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(pizzaInfo: PizzaInfo!): String + } + + input PizzaInfo { + name: String! + pineapples: Boolean! = false @deprecated(reason: "Don't need this input field") + } + ''' + + when: + TestUtil.schema(sdl) + + then: + noExceptionThrown() + } + + def "required field argument cannot be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(name: String!, pineapples: Boolean! @deprecated(reason: "Don't need this field argument")): String + } + ''' + + when: + TestUtil.schema(sdl) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.getErrors().size() == 1 + schemaProblem.getErrors().first().description == "Required argument 'pineapples' on field 'updatePizza' cannot be deprecated." + } + + def "multiple required field arguments cannot be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(name: String! @deprecated(reason: "yeah nah"), pineapples: Boolean! @deprecated(reason: "Don't need this field argument")): String + } + ''' + + when: + TestUtil.schema(sdl) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.getErrors().size() == 2 + schemaProblem.getErrors()[0].description == "Required argument 'name' on field 'updatePizza' cannot be deprecated." + schemaProblem.getErrors()[1].description == "Required argument 'pineapples' on field 'updatePizza' cannot be deprecated." + } + + def "nullable field argument can be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(name: String!, pineapples: Boolean @deprecated(reason: "Don't need this field argument")): String + } + ''' + + when: + TestUtil.schema(sdl) + + then: + noExceptionThrown() + } + + def "non-nullable field argument with default value can be deprecated"() { + def sdl = ''' + type Query { + pizza(name: String!): String + } + + type Mutation { + updatePizza(name: String!, pineapples: Boolean! = false @deprecated(reason: "Don't need this field argument")): String + } + ''' + + when: + TestUtil.schema(sdl) + + then: + noExceptionThrown() + } + + def "required directive argument cannot be deprecated"() { + def sdl = ''' + directive @pizzaDirective(name: String!, likesPineapples: Boolean! @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION + + type Query { + pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false) + } + + type Mutation { + updatePizza(name: String!, pineapples: Boolean!): String + } + ''' + + when: + TestUtil.schema(sdl) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.getErrors().size() == 1 + schemaProblem.getErrors().first().description == "Required argument 'likesPineapples' on directive 'pizzaDirective' cannot be deprecated." + } + + def "multiple required directive arguments cannot be deprecated"() { + def sdl = ''' + directive @pizzaDirective(name: String! @deprecated, likesPineapples: Boolean! @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION + + type Query { + pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false) + } + + type Mutation { + updatePizza(name: String!, pineapples: Boolean!): String + } + ''' + + when: + TestUtil.schema(sdl) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.getErrors().size() == 2 + schemaProblem.getErrors()[0].description == "Required argument 'name' on directive 'pizzaDirective' cannot be deprecated." + schemaProblem.getErrors()[1].description == "Required argument 'likesPineapples' on directive 'pizzaDirective' cannot be deprecated." + } + + def "nullable directive argument can be deprecated"() { + def sdl = ''' + directive @pizzaDirective(name: String!, likesPineapples: Boolean @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION + + type Query { + pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false) + } + + type Mutation { + updatePizza(name: String!, pineapples: Boolean!): String + } + + ''' + + when: + TestUtil.schema(sdl) + + then: + noExceptionThrown() + } + + def "non-nullable directive argument with default value can be deprecated"() { + def sdl = ''' + directive @pizzaDirective(name: String!, likesPineapples: Boolean! = false @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION + + type Query { + pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false) + } + + type Mutation { + updatePizza(name: String!, pineapples: Boolean!): String + } + + ''' + + when: + TestUtil.schema(sdl) + + then: + noExceptionThrown() + } + +} diff --git a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy index 6efd8acbe8..706542df8d 100644 --- a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy +++ b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy @@ -11,7 +11,7 @@ class SchemaValidatorTest extends Specification { def validator = new SchemaValidator() def rules = validator.rules then: - rules.size() == 8 + rules.size() == 9 rules[0] instanceof NoUnbrokenInputCycles rules[1] instanceof TypesImplementInterfaces rules[2] instanceof TypeAndFieldRule @@ -20,5 +20,6 @@ class SchemaValidatorTest extends Specification { rules[5] instanceof AppliedDirectiveArgumentsAreValid rules[6] instanceof InputAndOutputTypesUsedAppropriately rules[7] instanceof OneOfInputObjectRules + rules[8] instanceof DeprecatedInputObjectAndArgumentsAreValid } }