Kick-start your GraphQL server development.
KumuluzEE GraphQL project enables you to easily create your own GraphQL server with a few simple annotations and is fully compliant with MicroProfile GraphQL Sepcification. Using this extension requires understanding of the basic GraphQL concepts.
Read about GraphQL: GraphQL.
This project is built upon the SmallRye GraphQL implementation.
For 1.0.x users, see the following README: kumuluzee-graphql
For new users, using MicroProfile based implementation (this README) is recommended.
You can enable KumuluzEE GraphQL by adding the following dependency to the project:
<dependency>
<groupId>com.kumuluz.ee.graphql</groupId>
<artifactId>kumuluzee-graphql-mp</artifactId>
<version>${kumuluzee-graphql.version}</version>
</dependency>
When KumuluzEE GraphQL is included in the project, you can start developing your GraphQL services.
The @GraphQLApi
annotation must be used on the classes that define GraphQL related functions (queries, mutations, etc.).
All GraphQL annotated functions in annotated classes will be added to your GraphQL schema.
@GraphQLApi
public class CustomerResource {...}
The @Query
annotation will register your Java function as a Query function in GraphQL. All types and
parameters will be automatically converted to GraphQL types and added to the schema. You can override the query name
(which defaults to the function name without get
or set
prefix) or add a description to the query.
@GraphQLApi
public class HelloWorld {
@Query("helloWorld")
public String hello() {
return "Hello world!";
}
@Query
@Name("greet")
@Description("Greets person.")
public String sayHello(String person) {
return "Hello " + person + "!";
}
}
The @Mutation
annotation is used for defining mutations. It is used the same way as @Query
annotation.
The only difference is, that mutations are used for changing persistent state, while queries only retrieve data.
More information on this can be found in GraphQL documentation: Queries and mutations.
@GraphQLApi
public class CustomerResource {
// ...
@Mutation
public Customer saveCustomer(Customer customer) {
return customerService.save(customer);
}
@Mutation
@Name("saveOrder")
@Description("Saves the order to the database.")
public String newOrder(Order order) {
return orderService.save(order);
}
}
The name of GraphQL argument (on query or mutation) can be overridden with @Name
annotation. It can also be marked as
non-nullable with @NonNull
annotation or assigned a default value with @DefaultValue
annotation.
@Query
public Integer getCustomerCount(@Name("onlyRegistered") Boolean registered) {
return customerService.getCustomerCount(registered);
}
Avoid using primitive types as parameters (int, double...), because they cannot be
null
. If you use them, please provide their default values with@DefaultValue
annotation.
This annotation can be used to ignore a certain field.
public class Customer {
@Ignore
private String address;
}
If you want to mark a parameter as required, you can annotate the type with @NonNull
annotation.
It can be also used on lists:
// non null list of non null students
@NonNull List<@NonNull Student>
@Mutation
public String someMutation(@NonNull String field) {
return field;
}
The @Source
annotation can be used to define a resolver function for additional fields. The example below adds a
new field referrer
(of type String
) on the Customer
type:
@GraphQLApi
public class CustomerResource {
@Name("referrer")
public String getReferrerForCustomer(@Source Customer customer) {
return refererApi.getReferer(customer);
}
}
The @Source
annotation can also be used to resolve fields in batches. This is commonly referred to as the dataloader
pattern and is used to solve the N+1 problem. The following example would generate exactly the same schema as the example
above. The only difference is that in the example above the method is called once for every customer returned and in the
following example the method is called once for all customers that are returned.
@GraphQLApi
public class CustomerResource {
@Name("referrer")
public List<String> getReferrerForCustomer(@Source List<Customer> customers) {
return refererApi.getReferersForMultipleCustomers(customers);
}
}
Another use of the @Source
annotation is defining nested queries on types. For example:
@GraphQLApi
public class CustomerResource {
@Name("paidOrders")
public List<Order> getPaidOrders(@Source Customer customer) {
return customer.getOrders().stream()
.filter(o -> o.isPaid()).collect(Collectors.toList());
}
}
Nested queries can also be batched (dataloader pattern). This will generate the same schema (and functionality) as the example above:
@GraphQLApi
public class CustomerResource {
@Name("paidOrders")
public List<List<Order>> getPaidOrders(@Source List<Customer> customers) {
return customers.stream.map(c -> c.getOrders().stream()
.filter(o -> o.isPaid()).collect(Collectors.toList()))
.collect(Collectors.toList());
}
}
Exceptions can be thrown during query/mutation execution. The response will have the structure of the GraphQL error as defined in the GraphQL specification.
By default, all messages from unchecked exceptions (except some defaults, see below) will be hidden for security reasons. You can override this behavior
with the configuration key kumuluzee.graphql.exceptions.show-error-message
. The message will be replaced with
Server Error
and can be set using the configuration key kumuluzee.graphql.exceptions.default-error-message
.
By default, all messages from checked exceptions will be shown. You can hide messages from exceptions with the
configuration key kumuluzee.graphql.exceptions.hide-error-message
. Example configuration:
kumuluzee:
graphql:
exceptions:
hide-error-message:
- com.example.exceptions.HiddenCheckedException
show-error-message:
- com.example.exceptions.ShownRuntimeException
default-error-message: Server error, for more information contact ustomer service.
To provide a more seamless integration with kumuluzee-rest, some exceptions are added to show-error-message
list by default, namely:
- com.kumuluz.ee.rest.exceptions.InvalidEntityFieldException
- com.kumuluz.ee.rest.exceptions.InvalidFieldValueException
- com.kumuluz.ee.rest.exceptions.NoGenericTypeException
- com.kumuluz.ee.rest.exceptions.NoSuchEntityFieldException
- com.kumuluz.ee.rest.exceptions.QueryFormatException
To disable these defaults and handle everything manually use the following configuration:
kumuluzee:
graphql:
exceptions:
include-show-error-defaults: false
GraphQL endpoint (/graphql
) should be queried using a POST request. Request body should be a JSON object containing
field query
with the query that should be excecuted and optionally a field variables
containing a map of GraphQL
variables. For example:
HTTP POST localhost:8080/graphql
Header: Content-Type: application/json
Post data:
{
"query": "{customers {id, name, orders {id, total}}}",
"variables": {"myVariable": "someValue"}
}
GraphQL schema generated from annotations can be accessed by sending a GET request on /graphql/schema.graphql
endpoint. By default, some elements from the schema are omitted for readability. Additional information can be added to
schema by setting the following configuration keys to true
:
kumuluzee:
graphql:
schema:
include-scalars: true
include-schema-definition: true
include-directives: true
include-introspection-types: true
GraphQL server and schema will be served on /graphql/
by default. You can change this with the KumuluzEE configuration
framework by setting the following key:
kumuluzee:
graphql:
mapping: customers-api
By default KumuluzEE GraphQL uses optimized scanning in order to reduce startup times. This means that only the main
application JAR will be scanned (main artifact). In order to scan additional artifacts you need to specify them using
the scan-libraries mechanism. You need to include all dependencies
which contain GraphQL resources (annotated with @GraphQLApi
) as well as dependencies containing models returned from
GraphQL resources.
If all your models and resources are in the main artifact you don't need to include anything. For example to include
my-models artifact use the following configuration:
kumuluzee:
dev:
scan-libraries:
- my-models
If you are not sure if your configuration is correct you can try disabling scanning optimization. This will scan all dependencies but will drastically increase application startup time. Having this optimization disabled in production is not recommended. Disable optimized scanning by using the following configuration:
kumuluzee:
graphql:
scanning:
optimize: false
You can also enable scan debugging by setting the following key to true
: kumuluzee.graphql.scanning.debug
. This
will output a verbose log of scanning configuration and progress.
GraphiQL is a querying tool for GraphQL application. It is the Postman equivalent for GraphQL. You write your query, parameters and GraphiQL will send the request. It also checks your query syntax and allows you to explore your schema graphically. More information can be found here.
If you want to include GraphiQL to your project, include the following dependency:
<dependency>
<groupId>com.kumuluz.ee.graphql</groupId>
<artifactId>kumuluzee-graphql-ui</artifactId>
<version>${kumuluzee-graphql.version}</version>
</dependency>
By default, GraphiQL will be accessible on /graphiql
endpoint. You can configure the mapping or disable GraphiQL
with KumuluzEE Configuration framework. Example configuration:
kumuluzee:
graphql:
ui:
mapping: /api-ui
enabled: false
You can use kumuluzee-security extension to secure GraphQL queries and
mutations with familiar annotations @RolesAllowed
, @PermitAll
, etc. In order to start using kumuluzee-security first
create a class that extends GraphQLApplication
class and annotate it with @GraphQLApplication
and @DeclareRoles
.
For example:
@GraphQLApplicationClass
@DeclareRoles({"user", "admin"})
public class CustomerApp extends GraphQLApplication {
}
Then secure a class annotated with @GraphQLApi
by adding @Secure
annotation. You can then proceed to use the
standard @DenyAll
, @PermitAll
and @RolesAllowed
annotations. For example:
@RequestScoped
@GraphQLApi
@Secure
public class CustomerResource {
@Inject
private CustomerService customerBean;
@Query
@PermitAll
public List<Customer> getAllCustomers() {
return customerBean.getCustomers();
}
@Query
@RolesAllowed({"user", "admin"})
public Customer getCustomer(@Name("customerId") String customerId) {
return customerBean.getCustomer(customerId);
}
}
For a more detailed example of kumuluzee-security integration check out the kumuluzee-graphql-jpa-security sample.
You can enable automatic metrics integration by setting the following configuration key (note that
kumuluzee-metrics-core
dependency must be present):
kumuluzee:
graphql:
metrics:
enabled: true
This will add a counter and a timer to every query and mutation in the application. For a more fine-grained control over metrics you can always use metrics annotations on your query/mutation methods. For example:
@Query
@Counted(name = "get_customer_counter")
public Customer getCustomer(@Name("customerId") String customerId) {
return customerBean.getCustomer(customerId);
}
You can validate arguments to queries and mutations by enabling Bean Validation integration with the following
configuration key (note that kumuluzee-bean-validation-hibernate-validator
dependency must be present):
kumuluzee:
graphql:
bean-validation:
enabled: true
Arguments in query and mutation methods will then be verified by bean validation implementation. For example:
@Query
public Customer getCustomer(@Name("customerId") @Pattern(regexp = "\\d+") String customerId) {
return customerBean.getCustomer(customerId);
}
Another example:
@Mutation
public Customer addNewCustomer(@Name("customer") Customer customer) {
customerBean.saveCustomer(customer);
return customer;
}
// Customer.java:
public class Customer {
// ...
@Size(min = 3, max = 15)
private String firstName;
// ...
}
You can use the standard kumuluzee-rest parameters (pagination/sort/filter)
in GraphQL queries by using the GraphQLUtils.queryParametersBuilder()
to construct QueryParameters
which can then be used by kumuluzee-rest.
For example:
@Query
public StudentConnection getStudentsConnection(Long limit, Long offset, String sort, String filter) {
QueryParameters qp = GraphQLUtils.queryParametersBuilder()
.withQueryStringDefaults(qsd)
.withLimit(limit)
.withOffset(offset)
.withOrder(sort)
.withFilter(filter)
.build();
return new StudentConnection(JPAUtils.queryEntities(em, Student.class, qp),
JPAUtils.queryEntitiesCount(em, Student.class, qp));
}
Query:
query StudentsStartingWithJ {
studentsConnection(
offset: "0"
limit: "10"
sort: "studentNumber"
filter: "name:LIKE:J%"
) {
totalCount
edges {
studentNumber
name
surname
}
}
}
Recent changes can be viewed on Github on the Releases Page.
For 1.0.x users, see the following README: kumuluzee-graphql
See the contributing docs.
When submitting an issue, please follow the guidelines.
When submitting a bugfix, write a test that exposes the bug and fails before applying your fix. Submit the test alongside the fix.
When submitting a new feature, add tests that cover the feature.
MIT