Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit da01402

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents ed258ad + 415c899 commit da01402

File tree

102 files changed

+1711
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+1711
-1
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@ com_crashlytics_export_strings.xml
5959
crashlytics.properties
6060
crashlytics-build.properties
6161
fabric.properties
62+
63+
# Mac
64+
65+
.DS_Store

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- [Using Maven](#using-maven)
1313
- [Documentation](#documentation)
1414
- [Requirements and Downloads](#requirements-and-downloads)
15+
- [Snapshots](#snapshots)
1516
- [Enable GraphQL Servlet](#enable-graphql-servlet)
1617
- [Enable Graph*i*QL](#enable-graphiql)
1718
- [Enable Altair](#enable-altair)
@@ -23,6 +24,12 @@
2324
- [Tabs](#tabs)
2425
- [Supported GraphQL-Java Libraries](#supported-graphql-java-libraries)
2526
- [GraphQL Java Tools](#graphql-java-tools)
27+
- [GraphQL Annotations](#graphql-annotations)
28+
- [Configuration](#configuration)
29+
- [Root resolvers, directives, type extensions](#root-resolvers-directives-type-extensions)
30+
- [Interfaces](#interfaces)
31+
- [Custom scalars and type functions](#custom-scalars-and-type-functions)
32+
- [Custom Relay and GraphQL Annotation Processor](#custom-relay-and-graphql-annotation-processor)
2633
- [Tracing and Metrics](#tracing-and-metrics)
2734
- [Usage](#usage)
2835
- [Contributions](#contributions)
@@ -398,6 +405,65 @@ graphql:
398405
By default GraphQL tools uses the location pattern `**/*.graphqls` to scan for GraphQL schemas on the classpath.
399406
Use the `schemaLocationPattern` property to customize this pattern.
400407

408+
## GraphQL Annotations
409+
410+
https://github.com/Enigmatis/graphql-java-annotations
411+
412+
The schema will be built using the GraphQL Annotations library in a code-first approach - instead of writing it
413+
manually, the schema will be constructed based on the Java code. Please see the documentation of the GraphQL Annotations
414+
library for a detailed documentation of the available annotations. This readme focuses on how GraphQL Annotations -
415+
GraphQL Spring Boot Starter integration works.
416+
417+
### Configuration
418+
419+
```
420+
graphql:
421+
annotations:
422+
base-package: com.example.graphl.schema # required
423+
always-prettify: true #true is the default value, no need to specify it
424+
```
425+
426+
The most important parameter is the base package. The starter will look for schema-relevant classes in the specified
427+
package and its subpackages. `always-prettify` will "prettify" getter/setter methods - the get/set/is prefix will be
428+
removed from GraphQL fields automatically.
429+
430+
### Root resolvers, directives, type extensions
431+
432+
The root resolvers must be marked with the `GraphQLQueryResolver`, `GraphQLMutationResolver` and `GraphQLSubscription`
433+
annotations (not to be confused with the marker interfaces from the GraphQL Java Tools library).
434+
435+
**Important:**
436+
437+
Unlike GraphQL Java Tools, GraphQL Annotations only supports *one* of them each. Furthermore, GraphQL Annotations
438+
only accepts a *class* as input, *not an instance*. It will either create a new instance of the class itself, or use
439+
static methods. This means that Spring dependency injection will not work in the usual way. The companion example
440+
project (which can be found in the [samples](https://github.com/graphql-java-kickstart/samples) repository)
441+
demonstrates possible workarounds for this issue.
442+
443+
`GraphQLDirectiveDefinition` and `GraphQLTypeExtension`-annotated classes are subject to the same limitation regarding
444+
dependency injection - but there can be any number of them.
445+
446+
### Interfaces
447+
448+
Interfaces in the configured package having at least one of their methods marked as `@GraphQLField` are considered a
449+
GraphQL interface, and their implementations are automatically added to the schema. Furthermore, you have to add the
450+
following annotation to GraphQL interfaces: `@GraphQLTypeResolver(GraphQLInterfaceTypeResolver.class)`
451+
452+
### Custom scalars and type functions
453+
454+
Custom scalars can be defined in the same way as in the case of using GraphQL Java Tools - just define the
455+
`GraphQLScalarType` beans.
456+
457+
The starter will also pick up `TypeFunction` beans and pass them to the schema builder.
458+
459+
In these cases the actual beans will be used, not just the classes. Spring dependency injection works as usual.
460+
461+
### Custom Relay and GraphQL Annotation Processor
462+
463+
It is possible to define a bean implementing `Relay` and/or `GraphQLAnnotations`. If present, these will be passed to
464+
the schema builder. Spring dependency injection works as usual. Note that GraphQL Annotations provides default
465+
implementation for these which should be sufficient is most cases.
466+
401467
# Tracing and Metrics
402468
403469
[Apollo style tracing](https://github.com/apollographql/apollo-tracing) along with two levels of metrics based on them are currently configurable.
-6 KB
Binary file not shown.

gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ LIB_GRAPHQL_JAVA_VER = 14.1
4343
LIB_SPRING_BOOT_VER = 2.3.1.RELEASE
4444
LIB_GRAPHQL_SERVLET_VER = 9.2.0
4545
LIB_GRAPHQL_JAVA_TOOLS_VER = 6.1.0
46-
46+
LIB_GRAPHQL_ANNOTATIONS_VER = 8.1
47+
LIB_REFLECTIONS_VER = 0.9.11
4748
LIB_APACHE_COMMONS_TEXT=1.8
4849
LIB_JSOUP_VER=1.13.1
4950
kotlin.version=1.3.72
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
dependencies {
2+
api(project(":graphql-spring-boot-starter")) {
3+
exclude module: "graphql-kickstart-spring-boot-starter-tools-autoconfigure"
4+
exclude module: "graphql-java-tools"
5+
}
6+
api "io.github.graphql-java:graphql-java-annotations:$LIB_GRAPHQL_ANNOTATIONS_VER"
7+
implementation "org.reflections:reflections:$LIB_REFLECTIONS_VER"
8+
implementation "org.springframework.boot:spring-boot-autoconfigure"
9+
implementation "org.springframework.boot:spring-boot-starter-validation"
10+
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
11+
12+
testImplementation "org.springframework.boot:spring-boot-starter-test"
13+
testImplementation(project(":graphql-spring-boot-starter-test")) {
14+
exclude module: "graphql-kickstart-spring-boot-starter-tools-autoconfigure"
15+
exclude module: "graphql-java-tools"
16+
}
17+
testImplementation "io.reactivex.rxjava2:rxjava"
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import graphql.annotations.AnnotationsSchemaCreator;
4+
import graphql.annotations.annotationTypes.GraphQLField;
5+
import graphql.annotations.annotationTypes.GraphQLTypeExtension;
6+
import graphql.annotations.annotationTypes.directives.definition.GraphQLDirectiveDefinition;
7+
import graphql.annotations.processor.GraphQLAnnotations;
8+
import graphql.annotations.processor.typeFunctions.TypeFunction;
9+
import graphql.kickstart.graphql.annotations.exceptions.MissingQueryResolverException;
10+
import graphql.kickstart.graphql.annotations.exceptions.MultipleMutationResolversException;
11+
import graphql.kickstart.graphql.annotations.exceptions.MultipleQueryResolversException;
12+
import graphql.kickstart.graphql.annotations.exceptions.MultipleSubscriptionResolversException;
13+
import graphql.relay.Relay;
14+
import graphql.schema.GraphQLScalarType;
15+
import graphql.schema.GraphQLSchema;
16+
import lombok.RequiredArgsConstructor;
17+
import lombok.extern.slf4j.Slf4j;
18+
import org.reflections.Reflections;
19+
import org.reflections.ReflectionsException;
20+
import org.reflections.scanners.MethodAnnotationsScanner;
21+
import org.reflections.scanners.SubTypesScanner;
22+
import org.reflections.scanners.TypeAnnotationsScanner;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
28+
import java.lang.annotation.Annotation;
29+
import java.lang.reflect.Method;
30+
import java.util.Collections;
31+
import java.util.List;
32+
import java.util.Optional;
33+
import java.util.Set;
34+
35+
import static graphql.annotations.AnnotationsSchemaCreator.newAnnotationsSchema;
36+
37+
@Configuration
38+
@EnableConfigurationProperties(GraphQLAnnotationsProperties.class)
39+
@RequiredArgsConstructor
40+
@Slf4j
41+
public class GraphQLAnnotationsAutoConfiguration {
42+
43+
private final GraphQLAnnotationsProperties graphQLAnnotationsProperties;
44+
private final Optional<Relay> relay;
45+
private final List<TypeFunction> typeFunctions;
46+
private final List<GraphQLScalarType> customScalarTypes;
47+
48+
@Bean
49+
public GraphQLInterfaceTypeResolver graphQLInterfaceTypeResolver() {
50+
return new GraphQLInterfaceTypeResolver();
51+
}
52+
53+
@Bean
54+
@ConditionalOnMissingBean
55+
public GraphQLAnnotations graphQLAnnotations() {
56+
return new GraphQLAnnotations();
57+
}
58+
59+
@Bean
60+
public GraphQLSchema graphQLSchema(final GraphQLAnnotations graphQLAnnotations) {
61+
log.info("Using GraphQL Annotations library to build the schema. Schema definition files will be ignored.");
62+
log.info("GraphQL classes are searched in the following package (including subpackages): {}",
63+
graphQLAnnotationsProperties.getBasePackage());
64+
final AnnotationsSchemaCreator.Builder builder = newAnnotationsSchema();
65+
final Reflections reflections = new Reflections(graphQLAnnotationsProperties.getBasePackage(),
66+
new MethodAnnotationsScanner(), new SubTypesScanner(), new TypeAnnotationsScanner());
67+
builder.setAlwaysPrettify(graphQLAnnotationsProperties.isAlwaysPrettify());
68+
setQueryResolverClass(builder, reflections);
69+
setMutationResolverClass(builder, reflections);
70+
setSubscriptionResolverClass(builder, reflections);
71+
getTypesAnnotatedWith(reflections, GraphQLDirectiveDefinition.class).forEach(directive -> {
72+
log.info("Registering directive {}", directive);
73+
builder.directive(directive);
74+
});
75+
getTypesAnnotatedWith(reflections, GraphQLTypeExtension.class).forEach(typeExtension -> {
76+
log.info("Registering type extension {}", typeExtension);
77+
builder.typeExtension(typeExtension);
78+
});
79+
typeFunctions.forEach(typeFunction -> {
80+
log.info("Registering type function {}", typeFunction.getClass());
81+
builder.typeFunction(typeFunction);
82+
});
83+
if (!customScalarTypes.isEmpty()) {
84+
builder.typeFunction(new GraphQLScalarTypeFunction(customScalarTypes));
85+
}
86+
if (graphQLAnnotations.getClass().equals(GraphQLAnnotations.class)) {
87+
log.info("Using default GraphQL Annotation processor.");
88+
} else {
89+
log.info("Using custom annotation process of type {}", graphQLAnnotations.getClass());
90+
}
91+
builder.setAnnotationsProcessor(graphQLAnnotations);
92+
relay.ifPresent(r -> {
93+
log.info("Registering relay {}", r.getClass());
94+
builder.setRelay(r);
95+
});
96+
registerGraphQLInterfaceImplementations(reflections, builder);
97+
return builder.build();
98+
}
99+
100+
private void setSubscriptionResolverClass(
101+
final AnnotationsSchemaCreator.Builder builder,
102+
final Reflections reflections
103+
) {
104+
final Set<Class<?>> subscriptionResolvers
105+
= getTypesAnnotatedWith(reflections, GraphQLSubscriptionResolver.class);
106+
if (subscriptionResolvers.size() > 1) {
107+
throw new MultipleSubscriptionResolversException();
108+
}
109+
subscriptionResolvers.stream().findFirst().ifPresent(subscriptionClass -> {
110+
log.info("Registering subscription resolver class: {}", subscriptionClass);
111+
builder.subscription(subscriptionClass);
112+
});
113+
}
114+
115+
private void setMutationResolverClass(
116+
final AnnotationsSchemaCreator.Builder builder,
117+
final Reflections reflections
118+
) {
119+
final Set<Class<?>> mutationResolvers
120+
= getTypesAnnotatedWith(reflections, GraphQLMutationResolver.class);
121+
if (mutationResolvers.size() > 1) {
122+
throw new MultipleMutationResolversException();
123+
}
124+
mutationResolvers.stream().findFirst().ifPresent(mutationClass -> {
125+
log.info("Registering mutation resolver class: {}", mutationClass);
126+
builder.mutation(mutationClass);
127+
});
128+
}
129+
130+
private void setQueryResolverClass(
131+
final AnnotationsSchemaCreator.Builder builder,
132+
final Reflections reflections
133+
) {
134+
final Set<Class<?>> queryResolvers
135+
= getTypesAnnotatedWith(reflections, GraphQLQueryResolver.class);
136+
if (queryResolvers.size() == 0) {
137+
throw new MissingQueryResolverException();
138+
}
139+
if (queryResolvers.size() > 1) {
140+
throw new MultipleQueryResolversException();
141+
}
142+
queryResolvers.stream().findFirst().ifPresent(queryClass -> {
143+
log.info("Registering query resolver class: {}", queryClass);
144+
builder.query(queryClass);
145+
});
146+
}
147+
148+
/**
149+
* Workaround for a bug in Reflections - {@link Reflections#getTypesAnnotatedWith)} will throw a
150+
* {@link ReflectionsException} if there are no types with annotations in the specified package.
151+
* @param reflections the {@link Reflections} instance
152+
* @param annotation the annotation class
153+
* @return The set of classes annotated with the specified annotation, or an empty set if no annotated classes
154+
* found.
155+
*/
156+
private Set<Class<?>> getTypesAnnotatedWith(
157+
final Reflections reflections,
158+
final Class<? extends Annotation> annotation
159+
) {
160+
try {
161+
return reflections.getTypesAnnotatedWith(annotation);
162+
} catch (ReflectionsException e) {
163+
return Collections.emptySet();
164+
}
165+
}
166+
167+
/**
168+
* This is required, because normally implementations of interfaces are not explicitly returned by any resolver
169+
* method, and therefor not added to the schema automatically.
170+
*
171+
* All interfaces are considered GraphQL interfaces if they are declared in the configured package and
172+
* have at least one {@link GraphQLField}-annotated methods.
173+
*
174+
* @param reflections the reflections instance.
175+
* @param builder the schema builder instance.
176+
*/
177+
private void registerGraphQLInterfaceImplementations(
178+
final Reflections reflections,
179+
final AnnotationsSchemaCreator.Builder builder
180+
) {
181+
reflections.getMethodsAnnotatedWith(GraphQLField.class).stream()
182+
.map(Method::getDeclaringClass)
183+
.filter(Class::isInterface)
184+
.forEach(graphQLInterface ->
185+
reflections.getSubTypesOf(graphQLInterface)
186+
.forEach(implementation -> {
187+
log.info("Registering {} as an implementation of GraphQL interface {}", implementation,
188+
graphQLInterface);
189+
builder.additionalType(implementation);
190+
}));
191+
}
192+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
import org.springframework.boot.context.properties.ConfigurationProperties;
8+
import org.springframework.validation.annotation.Validated;
9+
10+
import javax.validation.constraints.NotBlank;
11+
12+
@Data
13+
@Builder
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
@Validated
17+
@ConfigurationProperties(prefix = "graphql.annotations")
18+
public class GraphQLAnnotationsProperties {
19+
20+
/**
21+
* The base package where GraphQL definitions (resolvers, types etc.) are searched for.
22+
*/
23+
private @NotBlank String basePackage;
24+
25+
/**
26+
* Set if fields should be globally prettified (removes get/set/is prefixes from names). Defaults to true.
27+
*/
28+
@Builder.Default
29+
private boolean alwaysPrettify = true;
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import graphql.TypeResolutionEnvironment;
4+
import graphql.annotations.processor.GraphQLAnnotations;
5+
import graphql.schema.GraphQLObjectType;
6+
import graphql.schema.TypeResolver;
7+
import org.springframework.beans.BeansException;
8+
import org.springframework.context.ApplicationContext;
9+
import org.springframework.context.ApplicationContextAware;
10+
11+
/**
12+
* Type resolver for GraphQL interfaces.
13+
* @see <a href="https://github.com/Enigmatis/graphql-java-annotations/issues/100">Issue with workaround.</a>
14+
*
15+
* Apply this interface to GraphQL interfaces using the {@link graphql.annotations.annotationTypes.GraphQLTypeResolver}
16+
* annotation.
17+
*/
18+
public class GraphQLInterfaceTypeResolver implements TypeResolver, ApplicationContextAware {
19+
20+
private static GraphQLAnnotations graphQLAnnotations;
21+
22+
@Override
23+
public GraphQLObjectType getType(final TypeResolutionEnvironment env) {
24+
return graphQLAnnotations.object(env.getObject().getClass());
25+
}
26+
27+
@Override
28+
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
29+
graphQLAnnotations = applicationContext.getBean(GraphQLAnnotations.class);
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target(ElementType.TYPE)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface GraphQLMutationResolver {
11+
}
12+

0 commit comments

Comments
 (0)