GORM GraphQL
Generates a GraphQL schema based on entities in GORM
Version: 2.0.0
1 Introduction
The GORM GraphQL library provides functionality to generate a GraphQL schema based on your GORM entities. If you aren’t sure what GraphQL is, please take a look at their homepage.
The underlying implementation of the GraphQL specification used by this library is graphql-java.
There are two different binaries available for use. The first is the core library which contains all of the logic for building the schema. The second is a Grails plugin which, on top of core, provides several features.
Core Library
In addition to mapping domain classes to a GraphQL schema, the core library also provides default implementations of "data fetchers" to query, update, and delete data through executions of the schema.
Because GraphQL is different in the way that the fields requested to be returned are included with the request, it allows the server to be smart about the query being executed. For example, when a property is requested, the server knows whether or not that property is an association. If it is an association, the query parameters can be set to eagerly fetch the association, resulting in a single query instead of many. The end result is that less queries will be executed against your database, greatly increasing the performance of your API.
Requires Java 8. Effort has been made to support GORM 6.0.x, however this library is only tested against 6.1.x. |
Grails Plugin
-
A controller to receive and respond to GraphQL requests through HTTP, based on their guidelines.
-
Generates the schema at startup with spring bean configuration to make it easy to extend.
-
Includes a GraphiQL browser enabled by default in development. The browser is accessible at
/graphql/browser
. -
Overrides the default data binder to use the data binding provided by Grails
-
Provides a trait to make integration testing of your GraphQL endpoints easier.
2 Installation
To use this library in your project, add the following dependency to the
dependencies
block of your build.gradle
:
For Grails applications
compile "org.grails.plugins:gorm-graphql-plugin:2.0.0"
For standalone projects
compile "org.grails:gorm-graphql:2.0.0"
3 Getting Started
Standalone Projects
For standalone projects, it is up to the developer how and when the schema gets created. A mapping context is required to create a schema. The mapping context is available on all datastore implementations. See the GORM documentation for information on creating a datastore.
The example below is the simplest way possible to generate a schema.
import org.grails.datastore.mapping.model.MappingContext
import org.grails.gorm.graphql.Schema
import graphql.schema.GraphQLSchema
MappingContext mappingContext = ...
GraphQLSchema schema = new Schema(mappingContext).generate()
Refer to the graphql-java documentation on how to use the schema to execute queries and mutations against it.
Grails Projects
For Grails projects, the schema is created automatically at startup. A spring bean is created for the schema called "graphQLSchema".
Using The Schema
By default, no domain classes will be a part of the generated schema. It is an opt in functionality that requires you to explicitly state which domain classes will be a part of the schema.
The simplest way to include a domain class in the schema is to add the following to your domain class.
static graphql = true
Just by adding the graphql = true
property on your domain, full CRUD capabilities will be available in the schema. For example, if the domain class is called Book
:
-
Queries
-
book(id: ..)
-
bookList(max: .., sort: .., etc)
-
bookCount
-
-
Mutations
-
bookCreate(book: {})
-
bookUpdate(id: .., book: {})
-
bookDelete(id: ..)
-
Practical Example
Imagine you are building an API for a Conference. A talk can be presented by a single speaker. A speaker can have many talks within the conference. A typical one-to-many relationship which in GORM could be expressed with:
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Speaker {
String firstName
String lastName
String name
String email
String bio
static hasMany = [talks: Talk]
static graphql = true (1)
static constraints = {
email nullable: true, email: true
bio nullable: true
}
static mapping = {
bio type: 'text'
name formula: 'concat(FIRST_NAME,\' \',LAST_NAME)'
talks sort: 'id'
}
}
1 | it exposes this domain class to the GraphQL API |
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Talk {
String title
int duration
static belongsTo = [speaker: Speaker]
}
GORM GraphQL plugin supports Derived Properties as illustrated in the previous example; name is derived property which concatenates firstName and lastName
|
3.1 Create
Create
In this example the request is a mutation to create a speaker.
curl -X "POST" "http://localhost:8080/graphql" \
-H "Content-Type: application/graphql" \
-d $'
mutation {
speakerCreate(speaker: {
firstName: "James"
lastName: "Kleeh"
}) {
id
firstName
lastName
errors {
field
message
}
}
}'
The API answers with the properties we requested.
{
"data": {
"speakerCreate": {
"id": 8,
"firstName": "James",
"lastName": "Kleeh",
"errors": [
]
}
}
}
If there was a validation error during the create process, the errors property would be populated with the validation errors.
|
3.2 Read
In this example the query is attempting to retrieve the details of a single speaker with an id of 1
.
curl -X "POST" "http://localhost:8080/graphql" \
-H "Content-Type: application/graphql" \
-d $'
{
speaker(id: 1) {
firstName
lastName
bio
}
}'
and the api responds with:
{
"data": {
"speaker": {
"firstName": "Jeff Scott",
"lastName": "Brown",
"bio": "Jeff is a co-founder of the Grails framework, and a core member of the Grails development team."
}
}
}
3.3 List
In this example the query is demonstrating retrieving a list of speakers. Since the talks of the speaker are requested to be returned, the association will be eagerly fetched to make the process as efficient as possible.
curl -X "POST" "http://localhost:8080/graphql" \
-H "Content-Type: application/graphql" \
-d $'
{
speakerList(max: 3) {
id
name
talks {
title
}
}
}'
which returns:
{
"data": {
"speakerList": [
{
"id": 1,
"name": "Jeff Scott Brown",
"talks": [
{
"title": "Polyglot Web Development with Grails 3"
},
{
"title": "REST With Grails 3"
},
{
"title": "Testing in Grails 3"
}
]
},
{
"id": 2,
"name": "Graeme Rocher",
"talks": [
{
"title": "What's New in Grails?"
},
{
"title": "The Latest and Greatest in GORM"
}
]
},
{
"id": 3,
"name": "Paul King",
"talks": [
{
"title": "Groovy: The Awesome Parts"
}
]
}
]
}
}
3.4 Count
In this example the query is demonstrating retrieving the total count of speakers.
curl -X "POST" "http://localhost:8080/graphql" \
-H "Content-Type: application/graphql" \
-d $'
{
speakerCount
}'
which returns:
{
"data": {
"speakerCount": 7
}
}
3.5 Update
In this example we are updating properties of a speaker.
curl -X "POST" "http://localhost:8080/graphql" \
-H "Content-Type: application/graphql" \
-d $'
mutation {
speakerUpdate(id: 7, speaker: {
bio: "Zachary is a member of the Grails team at OCI"
}) {
id
bio
talks {
id
duration
}
errors {
field
message
}
}
}'
and the server acknowledges the update:
{
"data": {
"speakerUpdate": {
"id": 7,
"bio": "Zachary is a member of the Grails team at OCI",
"talks": [
{
"id": 14,
"duration": 50
},
{
"id": 15,
"duration": 50
}
],
"errors": [
]
}
}
}
If there was a validation error during the update process, the errors property would be populated with the validation errors.
|
3.6 Delete
In this example we are deleting a speaker. None of the normal properties of a speaker are available, however a success
property is available to let you know whether or not the operation was successful.
curl -X "POST" "http://localhost:8080/graphql" \
-H "Content-Type: application/graphql" \
-d $'
mutation {
speakerDelete(id: 8) {
success
error
}
}'
and the server acknowledges the deletion:
{
"data": {
"speakerDelete": {
"success": true,
"error": null (1)
}
}
}
1 | The message of any exception that occurred will be returned if the action wasn’t successful. |
4 Customizing The Schema
The GraphQL specification allows for properties, types, operations, etc to have metadata associated with them. Metadata might include a description or deprecation. This library exposes APIs to allow the user to customize that metadata.
4.1 Properties
4.1.1 GORM Properties
Persistent Property Options
It is possible to control several aspects of how existing persistent properties on GORM entities are represented in the generated schema.
There are several ways to modify the fields available. Consider a property foo
:
import org.grails.gorm.graphql.entity.dsl.GraphQLMapping
import org.grails.gorm.graphql.entity.dsl.GraphQLPropertyMapping
static graphql = GraphQLMapping.build {
foo description: "Foo"
//or
foo {
description "Foo"
}
//or (code completion)
foo GraphQLPropertyMapping.build {
description "Foo"
}
//or
property('foo', description: "Foo")
//or (code completion)
property('foo') {
description "Foo"
}
//or (code completion)
property('foo', GraphQLPropertyMapping.build {
description "Foo"
})
}
Exclusion
To exclude a property from being included from the schema entirely:
import org.grails.gorm.graphql.entity.dsl.GraphQLMapping
class Author {
String name
static graphql = GraphQLMapping.build {
exclude('name')
}
}
To make a property read only:
static graphql = GraphQLMapping.build {
property('name', input: false)
}
To make a property write only:
static graphql = GraphQLMapping.build {
property('name', output: false)
}
Nullable
By default, a property will be nullable based on the constraints provided in the domain class. You can override that specifically for GraphQL, however.
static graphql = GraphQLMapping.build {
property('name', nullable: false) //or true
}
Fetching
To customize the way a property is retrieved, you can supply a data fetcher with a closure. The domain instance is passed as an argument to the closure.
static graphql = GraphQLMapping.build {
property('name') {
dataFetcher { Author author ->
author.name ?: "Default Name"
}
}
}
The data type returned must be the same as the property type. |
Description
A description of a property can be specified in the mapping to be registered in the schema.
static graphql = GraphQLMapping.build {
property('name', description: 'The name of the author')
}
Deprecation
A property can be marked as deprecated in the schema to inform users the property may be removed in the future.
static graphql = GraphQLMapping.build {
property('name', deprecationReason: 'To be removed August 1st, 2018')
//or
property('name', deprecated: true) //"Deprecated" will be the reason
}
Name
It is possible to change the name of existing properties as they appear in the schema.
static graphql = GraphQLMapping.build {
property('authority', name: 'name')
}
When changing the name of a property, you must also account for the change when it comes to data binding in create or update operations. The following is an example data binder implementation for changing the property name from authority to name .
|
import org.grails.gorm.graphql.plugin.binding.GrailsGraphQLDataBinder
class RoleDataBinder extends GrailsGraphQLDataBinder {
@Override
void bind(Object object, Map data) {
data.put('authority', data.remove('name'))
super.bind(object, data)
}
}
The data binding implementation is removing the name
property and assigning it back to authority
so it can be correctly bound to the domain object.
Order
The order in which the fields appear in the schema can be customized. By default any identity properties and the version property appear first in order.
To customize the order of an existing property:
static graphql = GraphQLMapping.build {
property('name', order: 1)
}
If the order is supplied via the constraints block, then that value will be used.
static constraints = {
name order: 1
}
If the order is specified in both the constraints and the property mapping, the property mapping wins. If the order of two or more properties is the same, they are then sorted by name. |
If no order is specified, the default order provided by GORM is used. To enable name based sorting by default, configure the default constraints to set the order property to 0 for all domains.
Example:
grails.gorm.default.constraints = {
'*'(order: 0)
}
To customize properties to come before the identifier or version, set the value as negative. The default order for id properties is -20 and the default order for version properties is -10. |
4.1.2 Custom Properties
Adding Custom Properties
It is possible to add custom properties to your domain class solely for the use in the GraphQL schema. You must supply either a custom data binder or a setter method for properties that will be used for input. For output, a data fetcher or getter method must be made available.
In this example we are adding a property to the GraphQL schema that will allow users to retrieve the age of authors. In this case the property doesn’t make sense to allow users to provide the property when creating or updating because they should be modifying the birthDay
instead. For that reason, input false
is specified to prevent that behavior.
Supplying other domain classes as the return type is supported |
import org.grails.gorm.graphql.entity.dsl.GraphQLMapping
import java.time.Period
class Author {
LocalDate birthDay
static graphql = GraphQLMapping.build {
add('age', Integer) {
dataFetcher { Author author ->
Period.between(author.birthDay, LocalDate.now()).years
}
input false
}
}
}
Instead of providing a data fetcher, it is possible to create a getter method to do the same thing. |
Integer getAge() {
Period.between(birthDay, LocalDate.now()).years
}
Returning A Collection
The above example creates a custom property that returns an Integer. The property were to return a collection, the following notation can be used:
add('age', [Integer]) {
...
}
Nullable, Description, Deprecation
Very similar to how existing domain properties can be configured, it is also possible to configure additional properties.
add('age', Integer) {
nullable false //default is true
description 'How old the author is in years'
deprecationReason 'To be removed in the future'
//or
deprecated true
}
Read or Write Only
Custom properties can also be controlled in the same way to existing properties in regards to whether they are read or write only.
To make a property read only:
add('name', String) {
input false
}
To make a property write only:
add('name', String) {
output false
}
Custom Type
If a property needs to handle something more complex than a collection of a single class, this library supports creating custom types.
For example if our age property needs to return years, months, and days:
import org.grails.gorm.graphql.entity.dsl.GraphQLMapping
import java.time.temporal.ChronoUnit
class Author {
LocalDate birthDay
Map getAge() {
LocalDate now = LocalDate.now()
[days: ChronoUnit.DAYS.between(birthDay, now),
months: ChronoUnit.MONTHS.between(birthDay, now),
years: ChronoUnit.YEARS.between(birthDay, now)]
}
static graphql = GraphQLMapping.build {
add('age', 'Age') {
input false
type {
field('days', Long)
field('months', Long)
field('years', Long)
}
}
}
}
In the above example we have added a new property to the domain class called age
. That property returns a custom data type that consists of 3 properties. The name that represents that type in the GraphQL schema is "Age". In our getter method we are returning a Map that contains those properties, however any POGO that contains those properties would work as well.
Similar to properties, the fields themselves can have complex subtypes with the field(String, String, Closure)
method signature.
For instance if we were to add a property to our domain to represent a list of books:
import org.grails.gorm.graphql.entity.dsl.GraphQLMapping
class Author {
//A getter method or dataFetcher is required to make this configuration valid
static graphql = GraphQLMapping.build {
add('books', 'Book') {
type {
field('title', String)
field('publisher', 'Publisher') {
field('name', String)
field('address', String)
}
collection true
}
}
}
}
When creating custom types, it is important that the name you choose does not already exist. For example if this application also had a Publisher domain class, the types will conflict because in GraphQL the type names must be unique. |
Order
The order in which the fields appear in the schema can be customized. By default any identity properties and the version property appear first in order.
To customize the order of a custom property:
static graphql = GraphQLMapping.build {
add('name', String) {
order 1
}
}
If no order is specified, added properties will be put at the end of the list of properties in the schema. Properties with the same or no order will be ordered by name.
To customize properties to come before the identifier or version, set the value as negative. The default order for id properties is -20 and the default order for version properties is -10. |
4.2 Operations
Provided Operations
The 3 query (get, list, count) and 3 mutation (create, update, delete) operations provided by default for all entities mapped with GraphQL are called the provided operations.
It is possible to customize the provided operations as well as disable their creation.
Disabling All Operations
static graphql = GraphQLMapping.build {
operations.all.enabled false
}
Disabling A Provided Operation
static graphql = GraphQLMapping.build {
operations.delete.enabled false
//or
//operations.get
//operations.list
//operations.count
//operations.create
//operations.update
//operations.delete
}
Disabling All Mutation(Write) Operation
static graphql = GraphQLMapping.build {
operations.mutation.enabled false
}
Disabling All Query(Read) Operation
static graphql = GraphQLMapping.build {
operations.query.enabled false
}
Metadata
The mapping closure also allows the description and deprecation status of the provided operations to be manipulated.
static graphql = GraphQLMapping.build {
operations.get
.description("Retrieve a single instance")
.deprecationReason("Use newBook instead")
operations.delete.deprecated(true)
}
Pagination
By default, list operations return a list of instances. For most pagination solutions, that is not enough data. In addition to the list of results, the total count is required to be able to calculate pagination controls. Pagination is supported in this library through configuration of the list operation.
static graphql = GraphQLMapping.build {
operations.list.paginate(true)
}
By configuring the list operation to paginate, instead of a list of results being returned, an object will be returned that has a "results" value, which is the list, and a "totalCount" value which is the total count not considering the pagination parameters.
Customization
It is possible to customize how the pagination response is created through the creation of a GraphQLPaginationResponseHandler.
How you can supply your own pagination response handler depends on whether the library is being used standalone or part of a Grails application.
For standalone applications, simply set the handler on the schema.
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.paginationResponseHandler = new MyPaginationResponseHandler()
...
schema.generate()
For Grails applications, override the graphQLPaginationResponseHandler
bean.
beans = {
graphQLPaginationResponseHandler(MyPaginationResponseHandler)
}
Data fetchers must respond according to the object type definition created by the pagination response handler. When supplying your own data fetcher, implement PaginatingGormDataFetcher and the response handler will be populated for you. The response handler is responsible for creating the response in order to assure it is compatible. |
Custom Operations
In addition to the provided operations, it is possible to add custom operations through the mapping block in an entity.
Custom operations can be defined with the query
method and the mutation
method, depending on what category the operation being added falls into. In both cases the API is exactly the same.
Definition
Custom operations must be defined in the mapping block of GORM entities. In the example below we have an Author
class that has a name
. We want to create a custom operation to retrieve an author by it’s name.
class Author {
String name
static graphql = GraphQLMapping.build {
//authorByName is the name exposed by the API
//Author is the return type
query('authorByName', Author) { //or [Author] to return a list of authors
...
}
}
}
In the case where a pagination result should be returned from the custom operation, a simple method pagedResult
is available to mark the type.
class Author {
String name
static graphql = GraphQLMapping.build {
//authorsByName is the name exposed by the API
//The return type will have a results key and a totalCount key
query('authorsByName', pagedResult(Author)) {
...
}
}
}
For operations with a custom return type, it possible to define a custom type using the returns
block. The API inside of the returns block is exactly the same as the API for defining custom properties with custom types.
mutation('deleteAuthors', 'AuthorsDeletedResponse') {
returns {
field('deletedCount', Long)
field('success', Boolean)
}
}
The data fetcher provided must have the defined fields.
Metadata
Description and deprecation information can also be supplied for custom operations.
query('authorByName', Author) { //or [Author] to return a list of authors
description 'Retrieves an author where the name equals the supplied name`
deprecated true
//or
deprecationReason 'Use authorWhereName instead`
}
Arguments
Arguments are the way users can supply data to your operation. The argument can be a simple type (String, Integer, etc), or it can also be a custom type that you define.
query('authorByName', Author) {
argument('name', String) //To take in a single string
argument('names', [String]) //To take in a list of strings
argument('name', 'AuthorNameArgument') { //A custom argument
accepts {
field('first', String)
field('last', String)
}
}
}
The API inside of the last argument block is exactly the same as the API for defining custom properties with custom types.
Argument Metadata
GraphQL has the ability to store metadata about arguments to operations.
query('authorByName', Author) {
argument('name', String) {
defaultValue 'John' //Supply a sensible default
nullable true //Allow a null value (default false)
description 'The name of the author to search for'
}
}
Data Fetcher
When creating a custom operation, it is necessary to supply a "data fetcher". The data fetcher is responsible for returning data to GraphQL to be used in generating the response. The data fetcher must be an instance of graphql.schema.DataFetcher
.
class Author {
String name
static hasMany = [books: Book]
static graphql = GraphQLMapping.build {
query('authorByName', Author) {
dataFetcher(new DataFetcher<>() {
@Override
Object get(DataFetchingEnvironment environment) {
Author.findByName(environment.getArgument('name'))
}
})
}
}
}
The above example will function properly, however it is missing out on one of the best features of this library, query optimization. If books were requested to be returned, a separate query would need to be executed to retrieve the books. To make this better, the recommendation is to always extend from one of the provided data fetchers.
Type |
Class |
GET |
|
LIST |
|
LIST (Paginated Response) |
|
CREATE |
|
UPDATE |
|
DELETE |
If the data fetcher you wish to create does not fit well in any of the above use cases, you can extend directly from DefaultGormDataFetcher, which has all of the query optimization logic.
All of the classes above have a constructor which takes in a PersistentEntity. The easiest way to get a persistent entity from a domain class is to execute the static gormPersistentEntity
method.
Using the above information, we can change the authorByName
to extend from the SingleEntityDataFetcher class because we are returning a single Author
.
class Author {
String name
static hasMany = [books: Book]
static graphql = GraphQLMapping.lazy {
query('authorByName', Author) {
argument('name', String)
dataFetcher(new SingleEntityDataFetcher<>(Author.gormPersistentEntity) {
@Override
protected DetachedCriteria buildCriteria(DataFetchingEnvironment environment) {
Author.where { name == environment.getArgument('name') }
}
})
}
}
}
Note the use of GraphQLMapping.lazy in this example. Because we are accessing the persistent entity, the GORM mapping context must be created before this code is evaluated. The lazy method will execute the provided code when the mapping is requested (during schema creation), instead of at class initialization time. By that time it is expected that GORM is available.
|
4.3 Validation Error and Delete Responses
The way the provided operations respond after deleting a GORM entity instance and how validation errors are returned is customizable.
Delete Response
Delete responses are handled by a GraphQLDeleteResponseHandler. The default implementation responds with an object that has a single property, success
. You can register your own handler to override the default.
For reference, see the default handler.
Standalone
After creating the schema, but before calling generate:
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.deleteResponseHandler = new MyDeleteResponseHandler()
Grails
To override the delete response handler in a Grails application, register a bean with the name "graphQLDeleteResponseHandler".
graphQLDeleteResponseHandler(MyDeleteResponseHandler)
Validation Error Response
Validation error responses are handled by a GraphQLErrorsResponseHandler.
For reference, see the default handler.
If you plan to extend the default handler, a message source is required.
Standalone
The errors response handler is a property of the type manager. If you are supplying a custom type manager to the schema, set the property on it directly. If you are relying on the default type manager, do the following after creating the schema, but before calling generate:
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.errorsResponseHandler = new MyErrorsResponseHandler()
Grails
To override the errors response handler in a Grails application, register a bean with the name "graphQLErrorsResponseHandler".
graphQLErrorsResponseHandler(MyErrorsResponseHandler, ref("messageSource"))
4.4 Naming Convention
Custom Naming Conventions
The names used to describe entities is configurable through the use of an GraphQLEntityNamingConvention.
The class controls the name of the default operations and the names of the GraphQL types built from the GORM entities.
Standalone
The naming convention is a property of the type manager. If you are supplying a custom type manager to the schema, set the property on it directly. If you are relying on the default type manager, do the following after creating the schema, but before calling generate:
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.namingConvention = new MyNamingConvention()
Grails
To override the naming convention in a Grails application, register a bean with the name "graphQLEntityNamingConvention" that extends GraphQLEntityNamingConvention.
graphQLEntityNamingConvention(MyNamingConvention)
4.5 Configuration
Standalone
The schema has several properties that can be modified.
List Arguments
A map where the key is a string (one of EntityDataFetcher.ARGUMENTS) and the value is the class the argument should be coerced into.
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.listArguments = (Map<String, Class>)..
Date Type
If a date type is not already registered with the type manager, a default one will be provided. The default type can be customized with the date formats that should be attempted to convert user input, and whether or not the parsing should be lenient.
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.dateFormats = (List<String>)...
schema.dateFormatLenient = (boolean)...
The date formats will also be used for the Java 8 date types. |
Plugin
The following options are available to configure for Grails applications using the plugin.
code |
Type |
Default Value |
Description |
|
Boolean |
True |
Whether the plugin is enabled |
|
List<String> |
- yyyy-MM-dd HH:mm:ss.S, - yyyy-MM-dd’T’HH:mm:ss’Z' - yyyy-MM-dd HH:mm:ss.S z - yyyy-MM-dd’T’HH:mm:ss.SSSX |
If the setting is not provided, the |
|
Boolean |
False |
If the setting is not provided, the |
|
Map<String, Class> |
The default arguments in EntityDataFetcher.ARGUMENTS |
|
|
Boolean |
True (only in development) |
Whether the |
Fetching Context
Data fetchers in GraphQL have a "context". The context is really just an Object field that can contain anything you like with the purpose of exposing it to the data fetching environment. For example, the default context created by the plugin is a Map that contains the request locale. The locale is used to render validation error messages.
If you prefer to set the context to a custom class, implement LocaleAwareContext and the default validation errors response handler will retrieve the locale from the object. |
It is common to need to manipulate the context to include data that some or all of your data fetchers might need. If a fetcher needs to be aware of the current user and you are using the spring security plugin, you may want to add the springSecurityService
to the context.
To customize the context, register a bean named "graphQLContextBuilder" of type GraphQLContextBuilder. If the default errors handler is being used, extend from DefaultGraphQLContextBuilder and add to the result of the super method call.
import org.grails.gorm.graphql.plugin.DefaultGraphQLContextBuilder
import org.springframework.beans.factory.annotation.Autowired
import grails.plugin.springsecurity.SpringSecurityService
class MyGraphQLContextBuilder extends DefaultGraphQLContextBuilder {
@Autowired
SpringSecurityService springSecurityService
@Override
Map buildContext(GrailsWebRequest request) {
Map context = super.buildContext(request)
context.springSecurityService = springSecurityService
context
}
}
graphQLContextBuilder(MyGraphQLContextBuilder)
Then in a data fetcher:
T get(DataFetchingEnvironment environment) {
((Map) environment.context).springSecurityService
}
5 Type Conversion And Creation
It is the responsibility of the type manager to convert Java classes to GraphQL types used in the creation of the schema. All of the basic GORM types have corresponding converters registered in the default type manager. It may be necessary to register GraphQL types for classes used in your domain model.
The type manager is designed to store GraphQL types for non GORM entities (simple types). If you encounter a TypeNotFoundException, it is likely you will need to register a type for the missing class. |
Get The Manager
To register type converters, you need to get a reference to the GraphQLTypeManager. If you are using the manager provided by default, how you access it will depend on whether you are using the plugin or standalone.
Standalone
When creating the schema, initialize it first. The default type manager will then be set.
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.initialize()
GraphQLTypeManager typeManager = schema.typeManager
...
schema.generate()
Plugin
For Grails applications it is recommended to reference the bean created by the plugin. The easiest way to do so is to register a bean post processor. The plugin has a class available to extend to make that easier.
myGraphQLCustomizer(MyGraphQLCustomizer)
import org.grails.gorm.graphql.plugin.GraphQLPostProcessor
class MyGraphQLCustomizer extends GraphQLPostProcessor {
@Override
void doWith(GraphQLTypeManager typeManager) {
...
}
}
If you need to customize more than 1 manager, only a single bean needs to be registered. There are doWith methods for all of the managers you may need to register object instances with.
|
Register A Converter
Once you have access to the manager, registration of your own type is easy. In this example a type is being registered to handle a Mongo ObjectId
. The type will be used to do conversion of arguments in GraphQL.
This process only handles how an ObjectId will be created from either Java arguments, or arguments passed inline in the GraphQL query or mutation. For Grails applications, the process of rendering the ObjectID as json is handled by JSON views. To supply behavior for how an ObjectId is rendered, see the documentation for json views. For standalone projects, you are responsible for any conversions that may need to happen. |
import graphql.schema.GraphQLScalarType
import org.bson.types.ObjectId
import graphql.schema.Coercing
GraphQLTypeManager typeManager
typeManager.registerType(ObjectId, new GraphQLScalarType("ObjectId", "Hex representation of a Mongo object id", new Coercing<ObjectId, ObjectId>() {
protected Optional<ObjectId> convert(Object input) {
if (input instanceof ObjectId) {
Optional.of((ObjectId) input)
}
else if (input instanceof String) {
parseObjectId((String) input)
}
else {
Optional.empty()
}
}
@Override
ObjectId serialize(Object input) {
convert(input).orElseThrow( {
throw new CoercingSerializeException("Could not convert ${input.class.name} to an ObjectId")
})
}
@Override
ObjectId parseValue(Object input) {
convert(input).orElseThrow( {
throw new CoercingParseValueException("Could not convert ${input.class.name} to an ObjectId")
})
}
@Override
ObjectId parseLiteral(Object input) {
if (input instanceof StringValue) {
parseObjectId(((StringValue) input).value).orElse(null)
}
else {
null
}
}
protected Optional<ObjectId> parseObjectId(String input) {
if (ObjectId.isValid(input)) {
Optional.of(new ObjectId(input))
}
else {
Optional.empty()
}
}
}))
6 Data Fetchers
This library provides a means for overriding the data fetchers used for the default provided operations. That is done through the use of a GraphQLDataFetcherManager.
Get The Manager
To register a fetcher, you need to get a reference to the GraphQLDataFetcherManager. If you are using the manager provided by default, how you access it will depend on whether you are using the plugin or standalone.
Standalone
When creating the schema, initialize it first. The default fetcher manager will then be set.
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.initialize()
GraphQLDataFetcherManager dataFetcherManager = schema.dataFetcherManager
...
schema.generate()
Plugin
For Grails applications it is recommended to reference the bean created by the plugin. The easiest way to do so is to register a bean post processor. The plugin has a class available to extend to make that easier.
myGraphQLCustomizer(MyGraphQLCustomizer)
import org.grails.gorm.graphql.plugin.GraphQLPostProcessor
class MyGraphQLCustomizer extends GraphQLPostProcessor {
@Override
void doWith(GraphQLDataFetcherManager dataFetcherManager) {
...
}
}
If you need to customize more than 1 manager, only a single bean needs to be registered. There are doWith methods for all of the managers you may need to register object instances with.
|
Register A Fetcher
Once you have access to the manager, registration of your own data fetcher is easy. In this example a data fetcher is being registered to handle soft deleting an entity.
Registering a fetcher for the Object type will allow it to be used for all domain classes.
|
GraphQLDataFetcherManager dataFetcherManager
fetcherManager.registerDeletingDataFetcher(Author, new DeleteEntityDataFetcher(Author.gormPersistentEntity) {
@Override
protected void deleteInstance(GormEntity instance) {
Author author = ((Author)instance)
author.active = false
author.save()
}
})
A class exists to use for soft deletes: SoftDeleteEntityDataFetcher |
There are 3 types of data fetchers that can be registered.
Binding
Binding data fetchers accept a data binder object to bind the argument(s) to the domain instance. When the binding data fetcher instances are retrieved from the manager, the data binder will be set automatically.
Deleting
Deleting data fetchers accept a delete response handler object that is responsible for determining how the fetcher should respond for delete requests. When the deleting data fetcher instances are retrieved from the manager, the delete response handler will be set automatically.
Reading
Reading data fetchers are designed to execute read only queries. They don’t require any other dependencies to work.
Query Optimization
All of the default data fetcher classes extend from DefaultGormDataFetcher. If you are creating your own custom data fetcher class, it is highly recommended to extend from it as well. The main reason for doing so is to take advantage of the built in query optimization. Based on what fields are requested in any given request, the default data fetchers will execute a query that will join any associations that have been requested. This feature ensures that each API call is as efficient as possible.
For example, consider the following domain structure:
class Author {
String name
static hasMany = [books: Book]
}
class Book {
static belongsTo = [author: Author]
}
When executing a query against a given book, if the author object is not requested, or if only the ID of the author is requested, the association will not be joined.
book(id: 5) {
author {
id
}
}
Example generated SQL:
select * from book b where id = 5
When any property on the author other than the ID is requested, then the association will be joined.
book(id: 5) {
author {
name
}
}
Example generated SQL:
select * from book b inner join author a on b.author_id = a.id where b.id = 5
The logic for retrieving the list of properties to be joined is contained within the EntityFetchOptions class. If you prefer not to extend from the default gorm data fetcher, it is possible to make use of this class in your own data fetcher.
Custom Property Data Fetchers
When adding a custom property to a domain class, it may be the case that a separate query is being executed on another domain class. Normally that query would not be able to benefit from the automatic join operations based on the requested fields in that domain class. When providing the closure to retrieve the relevant data, a second parameter is available. The second parameter to the closure will be an instance of ClosureDataFetchingEnvironment.
The environment is an extension of the GraphQL Java DataFetchingEnvironment
. In addition to everything the standard environment has, the environment also has methods for retrieving the fetch options or the list of join properties to use in your query.
In this example a property was added to the Tag
domain to retrieve the list of Post
objects that contain the given tag.
Set<Post> getPosts(Map queryArgs) {
Long tagId = this.id
Post.where { tags { id == tagId } }.list(queryArgs)
}
static graphql = GraphQLMapping.build {
add('posts', [Post]) {
input false
dataFetcher { Tag tag, ClosureDataFetchingEnvironment env ->
tag.getPosts(env.fetchArguments)
}
}
}
7 Data Binding
Data binders are responsible for taking the domain argument to a create or update operation and binding the data to an instance of the entity being created or updated.
Get The Manager
To register a data binders, you need to get a reference to the GraphQLDataBinderManager. If you are using the manager provided by default, how you access it will depend on whether you are using the plugin or standalone.
Standalone
When creating the schema, initialize it first. The default binder manager will then be set.
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.initialize()
GraphQLDataBinderManager dataBinderManager = schema.dataBinderManager
...
schema.generate()
Plugin
For Grails applications it is recommended to reference the bean created by the plugin. The easiest way to do so is to register a bean post processor. The plugin has a class available to extend to make that easier.
myGraphQLCustomizer(MyGraphQLCustomizer)
import org.grails.gorm.graphql.plugin.GraphQLPostProcessor
class MyGraphQLCustomizer extends GraphQLPostProcessor {
@Override
void doWith(GraphQLDataBinderManager dataBinderManager) {
...
}
}
If you need to customize more than 1 manager, only a single bean needs to be registered. There are doWith methods for all of the managers you may need to register object instances with.
|
Register A Data Binder
Once you have access to the manager, registration of your own data binder is easy. In this example a data binder is being registered for the Author domain class.
GraphQLDataBinderManager dataBinderManager
dataBinderManager.registerDataBinder(Author, new GraphQLDataBinder() {
@Override
void bind(Object object, Map data) {
Author author = (Author) object
author.name = data.name
}
})
Registering a data binder for the Object type will cause it to be executed when a data binder could not otherwise be found.
|
For Grails applications, a default data binder is supplied that uses the standard Grails data binding features. That will allow you to customize the binding via @BindUsing or any other mechanism available via that feature.
|
8 Interceptors
This library provides 2 types of interceptors, data fetcher and schema. Data fetcher interceptors are designed with the ability to prevent execution of a data fetcher. A common application of a data fetcher interceptor might be to implement security for the different operations available to be executed through GraphQL.
Schema interceptors allow users to hook directly into the schema creation process to modify the schema directly before it is created. This option is for users who are comfortable with the graphql-java library and would like to manipulate the schema as they see fit.
Any interceptor can implement Ordered to control the order it is invoked. |
Get The Manager
To register a data fetcher interceptor, you need to get a reference to the GraphQLInterceptorManager. If you are using the manager provided by default, how you access it will depend on whether you are using the plugin or standalone.
Standalone
When creating the schema, initialize it first. The default interceptor manager will then be set.
import org.grails.gorm.graphql.Schema
Schema schema = ...
schema.initialize()
GraphQLInterceptorManager interceptorManager = schema.interceptorManager
...
schema.generate()
Plugin
For Grails applications it is recommended to reference the bean created by the plugin. The easiest way to do so is to register a bean post processor. The plugin has a class available to extend to make that easier.
myGraphQLCustomizer(MyGraphQLCustomizer)
import org.grails.gorm.graphql.plugin.GraphQLPostProcessor
class MyGraphQLCustomizer extends GraphQLPostProcessor {
@Override
void doWith(GraphQLInterceptorManager interceptorManager) {
...
}
}
If you need to customize more than 1 manager, only a single bean needs to be registered. There are doWith methods for all of the managers you may need to register object instances with.
|
Data Fetcher Interceptors
All data fetcher interceptors must implement the GraphQLFetcherInterceptor interface. A base class is also available to extend from that allows all fetchers to proceed.
For all interceptor methods, if the return value is false
, further interceptors will not be executed and the resulting data will be null
.
Registering
Once you have access to the manager, registration of your own interceptor is easy. In this example a data fetcher interceptor is being executed for the Author domain class. The interceptor will get invoked for all operations executed for that domain.
GraphQLInterceptorManager interceptorManager
interceptorManager.registerInterceptor(Author, new AuthorFetcherInterceptor())
Registering an interceptor for the Object type will cause it to be executed for all domain classes.
|
Provided
There are two methods that will be invoked for provided data fetchers. Which one gets invoked depends on whether the operation is a query or a mutation. For query operations (GET, LIST), the onQuery
method will be executed. For mutation operations (CREATE, UPDATE, DELETE), the onMutation
method will be executed.
The data fetching environment and the data fetcher type are available to help make decisions about whether or not to interrupt the execution.
Custom
There are two methods that will be invoked for custom data fetchers. For operations registered with the query
method in the mapping, the onCustomQuery
method will be invoked. For operations registered with the mutation
method in the mapping, the onCustomMutation
method will be invoked.
For custom operations, the name of the operation is available to be able to distinguish one custom operation from another.
Schema Interceptors
In addition to data fetcher interceptors, it is also possible to register an interceptor for the GraphQL schema before it is built. These interceptors have the ability to intercept each persistent entity after its types are created, but before building, as well as one final interception before the schema as a whole is built.
Registration
Once you have access to the manager, registration of your own interceptor is easy. In this example a schema interceptor is being registered. The interceptor will get invoked for all entities as they are being created as well as once before the schema is finalized.
GraphQLInterceptorManager interceptorManager
interceptorManager.registerInterceptor(new MySchemaInterceptor())
Entity
As each entity is being processed, a list of query fields and a list of mutation fields are being built. Before those fields are passed to the root query or mutation object, any schema interceptors are invoked with the intention that they may mutate the lists of fields. The persistent entity is also available to the interceptor.
Final Schema
After all of the entities have been processed, schema interceptors are invoked with the query type and mutation type. Once again, this provides the opportunity to make changes before the types are built and passed to the root schema object.
9 Other Notes
Other Notes
This section of the documentation is designed to inform you about special circumstances that may impact some use cases.
Metadata
You may come across a time when you would like to supply metadata for a property or type, but because that type is not part of an exposed persistent entity, it is not possible to do so via the mapping DSL. For those use cases, a @GraphQL annotation exists. The annotation can be applied to related entities or enums, for example.
Nullability
The nullable constraint on persistent properties is only respected during the create operation. For all other cases, null is always allowed. The reasoning for allowing nulls for output operations (get or list) is due to the possibility of the properties being null as a result of validation errors during a create or update. Nulls are allowed for update operations to allow users to send a partial object instead of the entire object.
Inheritance
Because GraphQL doesn’t support the ability to have interfaces extend other interfaces, interface types will only be created for root entities that have children. All child entities, regardless of any intermediate parent classes, will set their interface type to their root entity interface.
When querying the root entity, all children will be available to select properties via the … on
directive.
Map Properties
Since `Map`s by definition are dynamic, they don’t fit well into the pre defined world that is the GraphQL schema. It is impossible to render them as they are with dynamic keys because the schema must be statically defined. There are a couple ways to work around this depending on how you are using the properties.
Truly Dynamic
For Map
properties where the keys are not known at all, it is entirely up to the user to determine how it should be handled. One example might be to return a list of objects instead. For example, consider the following domain class:
class Author {
//The key is the ISBN
Map<String, Book> books
static hasMany = [books: Book]
}
It is impossible to know what keys will be used in this example, so we will need to provide the data in a predefined format. Instead of a Map<String, Book>
, we could return the books as a List<Map>
where each map has a key
property and a value
property.
To accomplish this there are several steps necessary to take in order for everything to work property.
Configure The Property
The property must be modified to contain the definition proposed above. In order to change the data type of a property, it must first be excluded and then replaced with a custom property.
class Author {
//The key is the ISBN
Map<String, Book> books
static hasMany = [books: Book]
static graphql = GraphQLMapping.build {
exclude 'books' (1)
add('books', 'BookMap') { (2)
type { (3)
field('key', String)
field('value', Book)
collection true
}
dataFetcher { Author author ->
//author.books.entrySet() does not work here because
//the graphql-java implementation calls .get() on maps
author.books.collect { key, value -> (4)
[key: key, value: value]
}.sort(true, {a, b -> a.value.id <=> b.value.id})
}
}
}
}
1 | The persistent property is excluded from the schema |
2 | A custom property is created in its place |
3 | A custom type is defined for our property |
4 | The data is converted to the expected data type that GraphQL expects |
The domain class configuration is enough to be able to retrieve author instances with GraphQL. If users are to be creating or updating instances, there is more work to do.
Data Binding
A data binder must be configured to transform the data sent in the key/value
object format to a format that can be bound to the domain. For those using this library standalone, it is up to you to determine how that is best done. Grails data binding supports binding to Map types, given the request data is in a specific format. To achieve that format, a data binder for the Author
domain must be registered.
import groovy.transform.CompileStatic
import org.grails.gorm.graphql.plugin.binding.GrailsGraphQLDataBinder
@CompileStatic
class AuthorDataBinder extends GrailsGraphQLDataBinder {
@Override
void bind(Object object, Map data) {
List<Map> books = (List)data.remove('books')
for (Map entry: books) {
data.put("books[${entry.key}]".toString(), entry.value)
}
super.bind(object, data)
}
}
Then to register the data binder, see the section on data binders.
Pre Defined Keys
For Map
properties where the keys of the map are known, the process to handle them is much easier. In this example, there is a Map property on our Author
domain class that can store a lat
key and a long
key to represent latitude and longitude.
class Author {
Map homeLocation
static graphql = GraphQLMapping.build {
exclude 'homeLocation' (1)
add('homeLocation', 'Location') { (2)
type { (3)
field('lat', String)
field('long', String)
}
}
}
}
1 | The persistent property is excluded from the schema |
2 | A custom property is created in its place |
3 | A custom type is defined for our property |
All Together
Once all of the above is in place, here is what a create mutation might look like:
curl -X "POST" "http://localhost:8080/graphql" \
-H "Content-Type: application/graphql" \
-d $'
mutation {
authorCreate(author: {
name: "Sally",
homeLocation: {
lat: "41.101539",
long: "-80.653381"
},
books: [
{key: "0307887448", value: {title: "Ready Player One"}},
{key: "0743264746", value: {title: "Einstein: His Life and Universe"}}
]
}) {
id
name
homeLocation {
lat
long
}
books {
key
value {
id
title
}
}
errors {
field
message
}
}
}'
And here is the expected response:
{
"data": {
"authorCreate": {
"id": 1,
"name": "Sally",
"homeLocation": {
"lat": "41.101539",
"long": "-80.653381"
},
"books": [
{
"key": "0743264746",
"value": {
"id": 1,
"title": "Einstein: His Life and Universe"
}
},
{
"key": "0307887448",
"value": {
"id": 2,
"title": "Ready Player One"
}
}
],
"errors": [
]
}
}
}
10 API Docs
Click here to view the API Documentation.