Skip to content

Commit 998d188

Browse files
committed
Add schema inspection support on startup
Prior to this commit, a Spring for GraphQL application could be started with a schema and an incomplete set of data fetchers, as the schema would describe: * Queries/Mutations/Subscriptions that are not backed by any `@Controller` method, any Spring Data repository nor any custom `DataFetcher` * type fields that are not backed by any Java Type property nor any registered `DataFetcher` This problem can be noticed at runtime when a request is sent to the API. The response can contain a `null` field where data was expected, or even a GraphQL error because the field was non nullable. This often happens during development time while developers are implementing the schema. This commit adds a new `SchemaInspector` type that visits the GraphQL schema during the startup phase and looks into the `RuntimeWiring` for registered `DataFetcher` instances. Because data fetchers can be simple lambdas and do not require to expose a concrete return type, this also introduces a new `TypedDataFetcher` interface that returns a `ResolvableType`. This type is only declared by the data fetcher implementation, but does not necessarily reflects the concrete type of the returned instances. This inspection is best effort and has known limitations, such as Union types (those will not be inspected). Because of those, the inspection will not fail the application startup. The `SchemaInspector` collects all missing fields into a report and its output is logged at startup at the INFO level. As a first step, the inspector is package private and is only used by the `DefaultSchemaResourceGraphQlSourceBuilder`. The inspection cannot be disabled nor customized. We can expand this feature in future releases as the team collects feedback from the community. Closes gh-386
1 parent 0f9581d commit 998d188

File tree

7 files changed

+930
-9
lines changed

7 files changed

+930
-9
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data;
18+
19+
import graphql.schema.DataFetcher;
20+
import graphql.schema.DataFetchingEnvironment;
21+
22+
import org.springframework.core.ResolvableType;
23+
24+
/**
25+
* Specialized {@link DataFetcher} that can provide {@link ResolvableType type information}
26+
* about the {@link #get(DataFetchingEnvironment) instances returned}.
27+
* <p>Such {@code DataFetchers} are often backed by actual Java methods with declared return types.
28+
* Declared types might not reflect the concrete type of the returned instance.
29+
* @author Brian Clozel
30+
* @since 1.2.0
31+
*/
32+
public interface TypedDataFetcher<T> extends DataFetcher<T> {
33+
34+
/**
35+
* The type declared by this {@link DataFetcher}.
36+
* <p>The concrete type of the returned instance might differ from the declared one.
37+
* @return the declared type for the data to be fetched.
38+
*/
39+
ResolvableType getDeclaredType();
40+
41+
}

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@
5454
import org.springframework.core.KotlinDetector;
5555
import org.springframework.core.MethodIntrospector;
5656
import org.springframework.core.MethodParameter;
57+
import org.springframework.core.ResolvableType;
5758
import org.springframework.core.annotation.AnnotatedElementUtils;
5859
import org.springframework.core.convert.ConversionService;
5960
import org.springframework.expression.BeanResolver;
6061
import org.springframework.format.FormatterRegistrar;
6162
import org.springframework.format.support.DefaultFormattingConversionService;
6263
import org.springframework.format.support.FormattingConversionService;
6364
import org.springframework.graphql.data.GraphQlArgumentBinder;
65+
import org.springframework.graphql.data.TypedDataFetcher;
6466
import org.springframework.graphql.data.method.HandlerMethod;
6567
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
6668
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
@@ -538,7 +540,7 @@ public String toString() {
538540
/**
539541
* {@link DataFetcher} that wrap and invokes a {@link HandlerMethod}.
540542
*/
541-
static class SchemaMappingDataFetcher implements DataFetcher<Object> {
543+
static class SchemaMappingDataFetcher implements TypedDataFetcher<Object> {
542544

543545
private final MappingInfo info;
544546

@@ -629,6 +631,11 @@ private <T> Publisher<T> handleSubscriptionError(
629631
.flatMap(errors -> Mono.error(new SubscriptionPublisherException(errors, ex)));
630632
}
631633

634+
@Override
635+
public ResolvableType getDeclaredType() {
636+
return ResolvableType.forMethodReturnType(this.info.getHandlerMethod().getMethod());
637+
}
638+
632639
}
633640

634641

spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.data.util.TypeInformation;
4242
import org.springframework.graphql.data.GraphQlArgumentBinder;
4343
import org.springframework.graphql.data.GraphQlRepository;
44+
import org.springframework.graphql.data.TypedDataFetcher;
4445
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
4546
import org.springframework.util.Assert;
4647
import org.springframework.validation.BindException;
@@ -441,7 +442,7 @@ public interface ReactiveQueryByExampleBuilderCustomizer<T> {
441442
}
442443

443444

444-
private static class SingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<R> {
445+
private static class SingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements TypedDataFetcher<R> {
445446

446447
private final QueryByExampleExecutor<T> executor;
447448

@@ -480,10 +481,14 @@ public R get(DataFetchingEnvironment env) throws BindException {
480481
}).orElse(null);
481482
}
482483

484+
@Override
485+
public ResolvableType getDeclaredType() {
486+
return ResolvableType.forClass(this.resultType);
487+
}
483488
}
484489

485490

486-
private static class ManyEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<Iterable<R>> {
491+
private static class ManyEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements TypedDataFetcher<Iterable<R>> {
487492

488493
private final QueryByExampleExecutor<T> executor;
489494

@@ -522,10 +527,15 @@ public Iterable<R> get(DataFetchingEnvironment env) throws BindException {
522527
});
523528
}
524529

530+
@Override
531+
public ResolvableType getDeclaredType() {
532+
return ResolvableType.forClassWithGenerics(Iterable.class, this.resultType);
533+
}
534+
525535
}
526536

527537

528-
private static class ReactiveSingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<Mono<R>> {
538+
private static class ReactiveSingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements TypedDataFetcher<Mono<R>> {
529539

530540
private final ReactiveQueryByExampleExecutor<T> executor;
531541

@@ -564,10 +574,15 @@ public Mono<R> get(DataFetchingEnvironment env) throws BindException {
564574
});
565575
}
566576

577+
@Override
578+
public ResolvableType getDeclaredType() {
579+
return ResolvableType.forClassWithGenerics(Mono.class, this.resultType);
580+
}
581+
567582
}
568583

569584

570-
private static class ReactiveManyEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<Flux<R>> {
585+
private static class ReactiveManyEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements TypedDataFetcher<Flux<R>> {
571586

572587
private final ReactiveQueryByExampleExecutor<T> executor;
573588

@@ -606,6 +621,11 @@ public Flux<R> get(DataFetchingEnvironment env) throws BindException {
606621
});
607622
}
608623

624+
@Override
625+
public ResolvableType getDeclaredType() {
626+
return ResolvableType.forClassWithGenerics(Flux.class, this.resultType);
627+
}
628+
609629
}
610630

611631
}

spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import reactor.core.publisher.Flux;
3535
import reactor.core.publisher.Mono;
3636

37+
import org.springframework.core.ResolvableType;
3738
import org.springframework.core.convert.support.DefaultConversionService;
3839
import org.springframework.data.domain.Sort;
3940
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
@@ -46,6 +47,7 @@
4647
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
4748
import org.springframework.data.util.TypeInformation;
4849
import org.springframework.graphql.data.GraphQlRepository;
50+
import org.springframework.graphql.data.TypedDataFetcher;
4951
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
5052
import org.springframework.util.Assert;
5153
import org.springframework.util.LinkedMultiValueMap;
@@ -538,7 +540,7 @@ public interface ReactiveQuerydslBuilderCustomizer<T> {
538540
}
539541

540542

541-
private static class SingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<R> {
543+
private static class SingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements TypedDataFetcher<R> {
542544

543545
private final QuerydslPredicateExecutor<T> executor;
544546

@@ -581,10 +583,14 @@ public R get(DataFetchingEnvironment env) {
581583
}).orElse(null);
582584
}
583585

586+
@Override
587+
public ResolvableType getDeclaredType() {
588+
return ResolvableType.forClass(this.resultType);
589+
}
584590
}
585591

586592

587-
private static class ManyEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<Iterable<R>> {
593+
private static class ManyEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements TypedDataFetcher<Iterable<R>> {
588594

589595
private final QuerydslPredicateExecutor<T> executor;
590596

@@ -625,10 +631,15 @@ public Iterable<R> get(DataFetchingEnvironment env) {
625631
});
626632
}
627633

634+
@Override
635+
public ResolvableType getDeclaredType() {
636+
return ResolvableType.forClassWithGenerics(Iterable.class, this.resultType);
637+
}
638+
628639
}
629640

630641

631-
private static class ReactiveSingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<Mono<R>> {
642+
private static class ReactiveSingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements TypedDataFetcher<Mono<R>> {
632643

633644
private final ReactiveQuerydslPredicateExecutor<T> executor;
634645

@@ -670,10 +681,15 @@ public Mono<R> get(DataFetchingEnvironment env) {
670681
});
671682
}
672683

684+
@Override
685+
public ResolvableType getDeclaredType() {
686+
return ResolvableType.forClassWithGenerics(Mono.class, this.resultType);
687+
}
688+
673689
}
674690

675691

676-
private static class ReactiveManyEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<Flux<R>> {
692+
private static class ReactiveManyEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements TypedDataFetcher<Flux<R>> {
677693

678694
private final ReactiveQuerydslPredicateExecutor<T> executor;
679695

@@ -715,6 +731,11 @@ public Flux<R> get(DataFetchingEnvironment env) {
715731
});
716732
}
717733

734+
@Override
735+
public ResolvableType getDeclaredType() {
736+
return ResolvableType.forClassWithGenerics(Flux.class, this.resultType);
737+
}
738+
718739
}
719740

720741
}

spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultSchemaResourceGraphQlSourceBuilder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ protected GraphQLSchema initGraphQlSchema() {
139139
}
140140
});
141141

142+
SchemaInspector.Report schemaInspectionReport = new SchemaInspector().inspectSchema(registry, runtimeWiring);
143+
if(!schemaInspectionReport.isEmpty()) {
144+
logger.info(schemaInspectionReport.getSummary());
145+
logger.info(schemaInspectionReport.getDetailedReport());
146+
}
147+
142148
return (this.schemaFactory != null ?
143149
this.schemaFactory.apply(registry, runtimeWiring) :
144150
new SchemaGenerator().makeExecutableSchema(registry, runtimeWiring));

0 commit comments

Comments
 (0)