Skip to content

bherrmann2/graphql-kotlin

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GraphQL Kotlin

Build Status Awesome Kotlin Badge

Most GraphQL libraries for the JVM require developers to maintain two sources of truth for their GraphQL API, the schema and the corresponding code (data fetchers and types). Given the similarities between Kotlin and GraphQL, such as the ability to define nullable/non-nullable types, a schema should be able to be generated from Kotlin code without any separate schema specification. graphql-kotlin builds upon graphql-java to allow code-only GraphQL services to be built.

For information on GraphQL, please visit the GraphQL website.

For information on graphql-java, please visit GraphQL Java.

Getting started

Installation

Using a JVM dependency manager, simply link graphql-kotlin to your project.

With Maven:

<dependency>
  <groupId>com.expedia.www</groupId>
  <artifactId>graphql-kotlin</artifactId>
  <version>0.0.1</version>
</dependency>

With Gradle:

compile(group: 'com.expedia.www', artifact: 'graphql-kotlin', version: '0.0.1')

Generating a schema

bex-api-sdk provides a single function, toSchema, to generate a schema from Kotlin objects.

import graphql.schema.GraphQLSchema
import com.expedia.graphql.toSchema

class Query {
  fun getNumber() = 1
}

val schema: GraphQLSchema = toSchema(listOf(TopLevelObjectDef(Query())))

generates a GraphQLSchema with IDL that looks like this:

type TopLevelQuery {
  getNumber: Int!
}

The GraphQLSchema generated can be used to expose a GraphQL API endpoint.

Class TopLevelObjectDef

toSchema uses Kotlin reflection to build a GraphQL schema from given classes using graphql-java's schema builder. We don't just pass a KClass though, we have to actually pass an object, because the functions on the object are transformed into the query or mutation's data fetchers. In most cases, a TopLevelObjectDef can be constructed with just an object:

class Query {
  fun getNumber() = 1
}

val def = TopLevelObjectDef(query)

toSchema(listOf(def))

In the above case, toSchema will use query::class as the reflection target, and query as the data fetcher target.

In a lot of cases, such as with Spring AOP, the object (or bean) being used to generate a schema is a dynamic proxy. In this case, query::class is not Query, but rather a generated class that will confuse the schema generator. To specify the KClass to use for reflection on a proxy, pass the class to TopLevelObjectDef:

@Component
class Query {
  @Timed
  fun getNumber() = 1
}

val def = TopLevelObjectDef(query, Query::class)

toSchema(listOf(def))

More about writing schemas with Kotlin below.

Writing schemas with Kotlin

Basics

toSchema requires a list of TopLevelObjectDef objects for both queries and mutations to be included in the GraphQL schema.

A query type is simply a Kotlin class that specifies fields, which can be functions or properties:

class WidgetQuery {
  val widgetCount: Int
  fun widgetById(id: Int): Widget? {
    // grabs widget from a data source
  } 
}

will generate:

type TopLevelQuery {
  widgetCount: Int!
  widgetById(id: Int!): Widget
}

Any public properties and functions defined on a query or mutation Kotlin class will be translated into GraphQL fields on the object type. toSchema will recursively use Kotlin reflection to generate all object types, fields, arguments and enums.

Types

For the most part, graphql-kotlin can directly map most Kotlin "primitive" types to standard GraphQL scalar types:

Scalars

Kotlin Type GraphQL Type
kotlin.Int Int
kotlin.Long Long
kotlin.Short Short
kotlin.Float Float
kotlin.Double Float
kotlin.BigInteger BigInteger
kotlin.BigDecimal BigDecimal
kotlin.Char Char
kotlin.String String
kotlin.Boolean Boolean

graphql-kotlin also ships with a few extension scalar types:

Extension Scalars

  • UUID is a custom scalar type (string) that provides runtime validation to ensure the string is a valid v4 UUID. Fields and arguments of type java.util.UUID are mapped to this custom scalar.
  • URL is a custom scalar type (string) that provides runtime validation to ensure the string is a valid URL (protocol, host, port, file, etc). Fields and arguments of type java.util.URL are mapped to this custom scalar.

List Types

Both kotlin.Array and kotlin.collections.List are automatically mapped to the GraphQL List type. Type arguments provided to Kotlin collections are used as the type arguments in the GraphQL List type.

class SimpleQuery {
  val numbers = listOf(1, 2, 3)
}

The above Kotlin class would produce the following GraphQL schema:

schema {
  query: TopLevelQuery
}

type TopLevelQuery {
  numbers: List[Int!]
}

Nullability

Fields

Arguments

Enums

Interfaces

TBD

Subscriptions

TBD

Annotations

graphql-kotlin ships with a number of annotation classes to allow you to enhance your GraphQL schema for things that can't be directly derived from Kotlin reflection.

@Context

All GraphQL servers have a concept of a "context". A GraphQL context contains metadata that is useful to the GraphQL server, but shouldn't necessarily be part of the GraphQL query's API. A prime example of something that is appropriate for the GraphQL context would be trace headers for an OpenTracing system such as Zipkin or Haystack. The GraphQL query itself does not need the information to perform its function, but the server itself needs the information to ensure observability.

The contents of the GraphQL context vary across GraphQL applications. For JVM based applications, graphql-java provides a GraphQLContext interface that can be extended.

Simply add @Context to any argument to a field, and the GraphQL context for the environment will be injected. These arguments will be omitted by the schema generator.

class Query {
  fun doSomething(
    @Description("A value") value: Int,
    @Context context: MyGraphQLContextImpl
  ): Boolean! {
    doSomething(context.getResult());
    return true
  }
}

The above query would produce the following GraphQL schema:

schema {
  query: TopLevelQuery
}

type TopLevelQuery {
  doSomething(value: Int!): Boolean!
}

Note that the @Context annotated argument is not reflected in the GraphQL schema.

@Ignore

There are two ways to ensure the GraphQL schema generation omits fields when using Kotlin reflection:

The first is by marking the field as private scope. The second method is by annotating the field with @Ignore:

class Query {
  @Ignore
  val notPartOfSchema = "ignore me!"

  fun doSomething(
    value: Int
  ): Boolean! {
    return true
  }
}

The above query would produce the following GraphQL schema:

schema {
  query: TopLevelQuery
}

type TopLevelQuery {
  doSomething(value: Int!): Boolean!
}

Note that the public property notPartOfSchema is not included in the schema.

@Description

Since Javadocs are not available at runtime for introspection, graphql-kotlin includes an annotation class @Description that can be used to add schema descriptions to any GraphQL schema element:

@Description("A useful widget")
data class Widget(
  @Description("The widget's value")
  val value: Boolean?
)

class Query {
  @Description("Does something very special")
  fun doSomething(
    @Description("The special ingredient") value: Int
  ): Widget! {
    return Widget(value !== 1)
  }
}

The above query would produce the following GraphQL schema:

schema {
  query: TopLevelQuery
}

# A useful widget
type Widget {
  # The widget's value
  value: Boolean
}

type TopLevelQuery {
  # Does something very useful
  doSomething(
    # The special ingredient
    value: Int!
  ): Boolean!
}

Configuration

Documentation Enforcement

About

Code-only GraphQL schema generation for Kotlin

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Kotlin 98.9%
  • Other 1.1%