GORM GraphQL

Generates a GraphQL schema based on entities in GORM

Version: 2.0.0

1 Introduction

GraphQL

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:

grails-app/domain/demo/Speaker.groovy
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
grails-app/domain/demo/Talk.groovy
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-app/conf/application.groovy
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.

grails-app/config/spring/resources.groovy
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

SingleEntityDataFetcher

LIST

EntityDataFetcher

LIST (Paginated Response)

PaginatedEntityDataFetcher

CREATE

CreateEntityDataFetcher

UPDATE

UpdateEntityDataFetcher

DELETE

EntityDataFetcher

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".

resources.groovy
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".

resources.groovy
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.

resources.groovy
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

grails.gorm.graphql.enabled

Boolean

True

Whether the plugin is enabled

grails.gorm.graphql.dateFormats

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 grails.databinding.dateFormats will be used.

grails.gorm.graphql.dateFormatLenient

Boolean

False

If the setting is not provided, the grails.databinding.dateParsingLenient will be used.

grails.gorm.graphql.listArguments

Map<String, Class>

The default arguments in EntityDataFetcher.ARGUMENTS

grails.gorm.graphql.browser

Boolean

True (only in development)

Whether the GraphqlController.browser action which by default responds on the graphql/browser endpoint is enabled

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.

src/main/groovy/demo/MyGraphQLContextBuilder.groovy
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
    }
}
resources.groovy
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.

resources.groovy
myGraphQLCustomizer(MyGraphQLCustomizer)
src/main/groovy/demo/MyGraphQLCustomizer.groovy
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.

resources.groovy
myGraphQLCustomizer(MyGraphQLCustomizer)
src/main/groovy/demo/MyGraphQLCustomizer.groovy
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.

resources.groovy
myGraphQLCustomizer(MyGraphQLCustomizer)
src/main/groovy/demo/MyGraphQLCustomizer.groovy
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.

resources.groovy
myGraphQLCustomizer(MyGraphQLCustomizer)
src/main/groovy/demo/MyGraphQLCustomizer.groovy
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:

grails-app/domain/demo/Author.groovy
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.

grails-app/domain/demo/Author.groovy
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.

src/main/groovy/demo/AuthorDataBinder.groovy
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.

grails-app/domain/demo/Author.groovy
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.