muuki88 / sbt-graphql

SBT plugin to generate and validate graphql schemas written with Sangria
Apache License 2.0
95 stars 20 forks source link
graphql sbt sbt-plugin schema-generation schema-validation

sbt-graphql Build Status Download

This plugin is sbt 1.x only and experimental.

sbt plugin to generate and validate graphql schemas written with Sangria.

Goals

This plugin is intended for testing pipelines that ensure that your graphql schema and queries are intact and match. You should also be able to compare it with another schema, e.g. the production schema, to avoid breaking changes.

Features

All features are based on the excellent Sangria GraphQL library

Examples for client-side code generation and for schema validation can be found in the test-project directory.

Note: Generating server-side code from a schema is currently not supported. Look here for sangria-based solutions.

Usage

Add this to your plugins.sbt and replace the <version> placeholder with the latest release.

addSbtPlugin("de.mukis" % "sbt-graphql" % "<version>")

In your build.sbt enable the plugins and add sangria. I'm using circe as a parser for my JSON response.

enablePlugins(GraphQLSchemaPlugin, GraphQLQueryPlugin)

libraryDependencies ++= Seq(
  "org.sangria-graphql" %% "sangria" % "1.4.2"
)

Schema generation

The schema is generated by accessing the application code via a generated main class that renders your schema. The main class accesses your code via a small code snippet defined in graphqlSchemaSnippet.

Example: My schema is defined in an object called ProductSchema in a field named schema. In your build.sbt add

graphqlSchemaSnippet := "example.ProductSchema.schema"

Now you can generate a schema with

$ sbt graphqlSchemaGen

You can configure the output directory in your build.sbt with

target in graphqlSchemaGen := target.value / "graphql-build-schema"

Schema definitions

Your build can contain multiple schemas. They are stored in the graphqlSchemas setting. This allows to compare arbitrary schemas, write schema.json files for each of them and validate your queries against them.

There is already one schemas predefined. The build schema is defined by the graphqlSchemaGen task. You can configure the graphqlSchemas label with

name in graphqlSchemaGen := "local-build"

Add a schema

Schemas are defined via a GraphQLSchema case class. You need to define

You can also define a schema from a SchemaLoader. This requires defining an anonymous sbt task.

graphqlSchemas += GraphQLSchema(
  "sangria-example",
  "staging schema at http://try.sangria-graphql.org/graphql",
  Def.task(
    GraphQLSchemaLoader
      .fromIntrospection("http://try.sangria-graphql.org/graphql", streams.value.log)
      .loadSchema()
  ).taskValue
)

sbt-graphql provides a helper object GraphQLSchemaLoader to load schemas from different places.

// from a Json file
graphqlProductionSchema := GraphQLSchemaLoader
  .fromFile((resourceManaged in Compile).value / "prod.json")
  .loadSchema()

// from a GraphQL file
graphqlProductionSchema := GraphQLSchemaLoader
  .fromFile((resourceManaged in Compile).value / "prod.graphql")
  .loadSchema()

// from a graphql endpoint via introspection
graphqlProductionSchema := GraphQLSchemaLoader
  .fromIntrospection("http://prod.your-graphql.net/graphql", streams.value.log)
  .withHeaders("X-Api-Version" -> "1", "X-Api-Key" -> "4198ab84-e992-42b0-8742-225ed15a781e")
  .loadSchema()

// from a graphql endpoint via introspection with post request
graphqlProductionSchema := GraphQLSchemaLoader
  .fromIntrospection("http://prod.your-graphql.net/graphql", streams.value.log)
  .withPost()
  .loadSchema()

Schema comparison

Sangria provides an API for comparing two Schemas. A change can be breaking or not. The graphqlValidateSchema task compares two given schemas defined in the graphqlSchemas setting.

graphqlValidateSchema <new schema> <old schema>

Example

You can compare the build and prod schema with

$ sbt
> graphqlValidateSchema build prod

Schema rendering

You can render every schema with the graphqlRenderSchema task. In your sbt shell

> graphqlRenderSchema build

This will render the build schema.

You can configure the target directory with

target in graphqlRenderSchema := target.value / "graphql-schema"

Schema release notes

sbt-graphql creates release notes from changes between two schemas. The format is currently markdown.

$ sbt 
> graphqlReleaseNotes <new schema> <old schema>

Example

You can create release notes for the build and prod schema with

$ sbt
> graphqlReleaseNotes build prod

Code Generation

A graphql query result is usually modelled with case classes, enums and traits. Writing these query result classes is tedious and error prone. sbt-graphql can generate the correct models for every graphql query.

A lot of inspiration came from apollo codegen. Make sure to check it out for scalajs, typescript and plain javascript projects.

Configuration

Enable the code generation plugin in your build.sbt

enablePlugins(GraphQLCodegenPlugin)

You need a graphql schema for the code generation. The schema is necessary to figure out the types for each query field. By default, the codegen plugin looks for a schema at src/main/resources/schema.graphql.

We recommend to configure a graphql schema in your graphqlSchemas and use the task to render the schema to a specific file.

// add a 'starwars' schema to the `graphqlSchemas` list
graphqlSchemas += GraphQLSchema(
  "starwars",
  "starwars schema at http://try.sangria-graphql.org/graphql",
  Def.task(
    GraphQLSchemaLoader
      .fromIntrospection("http://try.sangria-graphql.org/graphql", streams.value.log)
      .withHeaders("User-Agent" -> s"sbt-graphql/${version.value}")
      .loadSchema()
  ).taskValue
)

// use this schema for the code generation
graphqlCodegenSchema := graphqlRenderSchema.toTask("starwars").value

The graphqlCodegenSchema requires a File that points to a valid graphql schema file. graphqlRenderSchema is a task that renders any given schema in the graphqlSchemas into a schema file. It takes as input, the unique label that identifies the schema. The toTask("starwars") invocation converts the graphqlRenderSchema input task with the input parameter starwars to a plain task that can be evaluated as usual with .value.

By default, all *.graphql files in your resourceDirectories will be used for code generation.

Settings

You can configure the output in various ways

JSON support

The common serialization format for graphql results and input variables is JSON. sbt-graphql supports JSON decoder/encoder code generation.

Supported JSON libraries and codegen styles

In your build.sbt you can configure the JSON library with

graphqlCodegenJson := JsonCodec.Circe
// or
graphqlCodegenJson := JsonCodec.PlayJson

Scalar types

The code generation doesn't know about your additional scalar types. sbt-graphql provides a setting graphqlCodegenImports to add an import to every generated query object.

Example:

scalar ZoneDateTime

which is represented as java.time.ZoneDateTime. Add this as an import


graphqlCodegenImports += "java.time.ZoneDateTime"

Code Gen directive

The plugin provides a codeGen directive that is erased during source code generation and can be used to customize the generated code.

Example - skip code generation

To skip the code generation and provide your own type.

query CodeGenHeroNameQuery {
  hero @codeGen(useType: "Hero") {
    name
  }
}

You can also use the fully qualified class name to avoid clashes

query CodeGenHeroNameQuery {
  hero @codeGen(useType: "com.example.model.Hero") {
    name
  }
}

Difference to scalar types

Use the code gen directive, when the code generation doesn't generate the code you need. This can have multiple reasons

The code gen directive is intended to work on object types.

Scalar types represent the core building blocks of your graphql schema like String, Float, Boolean, etc. (DateTime is unfortunately missing until now). For this reasons there's no code generation for those types, which makes the code gen directive obsolete to use.

Magic #imports

This is a feature tries to replicate the apollographql/graphql-tag loader.js feature, which enables including (or actually inlining) partials into a graphql query with magic comments.

Explained

The syntax is straightforward

#import path/to/included.fragment.graphql

The fragment files should be named liked this

<name>.fragment.graphql

There is a excludeFilter in graphqlCodegen, which removes them from code generation so they are just used for inlining and interface generation.

The resolving of paths works like this

Example

I have a file CharacterInfo.fragment.graphql which contains only a single fragment

fragment CharacterInfo on Character {
    name
}

And the actual graphql query file

query HeroFragmentQuery {
  hero {
    ...CharacterInfo
  }
  human(id: "Lea") {
    homePlanet
    ...CharacterInfo
  }
}

#import fragments/CharacterInfo.fragment.graphql

Codegen style Apollo

As the name suggests the output is similar to the one in apollo codegen.

A basic GraphQLQuery trait is generated, which all queries extend.

trait GraphQLQuery {
  type Document
  type Variables
  type Data
}

The Document contains the query document parsed with sangria. The Variables type represents the input variables for the particular query. The Data type represents the shape of the query result.

For each query a new object is created with the name of the query.

Example:

query HeroNameQuery {
  hero {
    name
  }
}

Generated code:

package graphql.codegen
import graphql.codegen.GraphQLQuery
import sangria.macros._
object HeroNameQuery {
  object HeroNameQuery extends GraphQLQuery {
    val document: sangria.ast.Document = graphql"""query HeroNameQuery {
  hero {
    name
  }
}"""
    case class Variables()
    case class Data(hero: Hero)
    case class Hero(name: Option[String])
  }
}

Interfaces, types and aliases

The ApolloSourceGenerator generates an additional file Interfaces.scala with the following shape:

object types {
   // contains all defined types like enums and aliases
}
// all used fragments and interfaces are generated as traits here
Use case

Share common business logic around a fragment that shouldn't be a directive

You can now do this by defining a fragment and include it in every query that requires to apply this logic. sbt-graphql will generate the common trait?, all generated case classes will extend this fragment trait.

Limitations

You need to copy the fragments into every graphql query that should use it. If you have a lot of queries that reuse the fragment and you want to apply changes, this is cumbersome.

You cannot nest fragments. The code generation isn't capable of naming the nested data structure. This means that you need create fragments for every nesting.

Invalid

query HeroNestedFragmentQuery {
  hero {
    ...CharacterInfo
  }
  human(id: "Lea") {
    ...CharacterInfo
  }
}

# This will generate code that may compile, but is not usable
fragment CharacterInfo on Character {
    name
     friends {
        name
    }
}

correct

query HeroNestedFragmentQuery {
  hero {
    ...CharacterInfo
  }
  human(id: "Lea") {
    ...CharacterInfo
  }
}

# create a fragment for the nested query
fragment CharacterFriends on Character {
    name
}

fragment CharacterInfo on Character {
    name
    friends {
        ...CharacterFriends
    }
}

Codegen Style Sangria

This style generates one object with a specified moduleName and puts everything in there.

Example:

query HeroNameQuery {
  hero {
    name
  }
}

Generated code:

object HeroNameQueryApi {
  case class HeroNameQuery(hero: HeroNameQueryApi.HeroNameQuery.Hero)
  object HeroNameQuery {
    case class HeroNameQueryVariables()
    case class Hero(name: Option[String])
  }
}

Query validation

The query validation uses the schema generated with graphqlSchemaGen to validate against all graphql queries defined under src/main/graphql. Using separated graphql files for queries is inspired by apollo codegen which generates typings for various languages.

To validate your graphql files run

sbt graphqlValidateQueries

You can change the source directory for your graphql queries with this line in your build.sbt

sourceDirectory in (Test, graphqlValidateQueries) := file("path/to/graphql")

Developing

Test project

You can try out your changes immediately with the test-project:

$ cd test-project
sbt

If you change code in the plugin you need to reload the test-project.

Releasing

Push a tag vX.Y.Z a travis will automatically release it. If you push to the snapshot branch a snapshot version (using the git sha) will be published.

The git.baseVersion := "x.y.z" setting configures the base version for snapshot releases.