GraphQL schema decorators
Jonas Helfer
Last week on Building Apollo, we announced our alpha release, complete with several demo apps and a tutorial for building a GraphQL server. This week I want to write about a really exciting feature that I’m working on — schema decorators.
Imagine if all you needed to build a GraphQL server was a schema declaration. No code in resolve functions, no custom logic, just a schema written in GraphQL schema language.
Or imagine that you want to add a functionality — like checking authorization or validating arguments — to different parts of your schema in a simple way.
Schema decorators will allow you to do both of these things and more! They enable you to add features to your schemas with reusable modules, and even make schema definitions portable across programming languages. With schema decorators, you can build reusable modules that do any of the following and more:
- Adding annotations/metadata to the schema
- Enforcing auth checks
- Argument validation
- Logging and profiling
- Error handling
- Connecting to backend data stores
- Filtering or sorting of results
Why do we need schema decorators?
Right now, GraphQL allows you to customize the way the server executes a query by adding directives , but there is no similar functionality for schemas. As a developer, you have only a handful of ways to modify a schema:
- You can create custom types and combine them into a schema.
- You can write arbitrary code inside resolve functions.
- You can add descriptions to types, fields and args, and you can deprecate fields.
GraphQL currently lacks a standard and modular way to add features to the schema.
What are schema decorators?
Schema decorators are similar to function decorators in Python or ES2016 — they alter the behavior of the thing they decorate. While Python decorators only apply to functions, our proposal for schema decorators can be selectively applied to arguments, fields, types or the schema.
Rather than just providing a way to add metadata to a schema, decorators also establish a standard way for defining the semantics of that metadata.
Goals for schema decorators:
- They must be flexible enough to allow a wide range of behaviors
- They must be well-specified, so it’s clear when and where they can be used
- They must be introspectable, such that the information is preserved after a parse/print cycle.
- They must be fully self-contained, meaning that it is not necessary to modify the execution engine to use them.
- There should be a core set of decorators that have the same meaning and function across implementations of GraphQL in different languages.
Schema decorators share some similarities with GraphQL directives, but they differ in a few key ways:
- Directives apply to queries, decorators apply to the schema
- Directives come after the thing they apply to, decorators come before it (as they do in every other programming language)
- Directives (currently) achieve their behavior through changes to the source code of the query execution engine, whereas decorators rely on a set of standard hooks.
How do schema decorators work?
The main benefit of schema decorators is that they can be used to add semantically meaningful metadata to a schema specified in GraphQL schema language:
// Sample GraphQL schema with decorators
+connector(storage: "MySQL")
+doc(description: "A Person")
type Person {
+mock(type: "ID")
id: Int
+mock(raw: "John")
name: String
+requireAuthRole(role: "admin")
age: Int
}
+log(info: "query")
type Query {
findPerson(name: String!): Person
+requireAuthRole(role: "admin")
allPeople(): [Person]
}
+log(info: "mutation")
+requireAuthRole(role: "admin")
type Mutation {
addPerson(
+validateLength(min: 1, max:100)
name: String!
): Person
removePerson(id: Int!): Boolean
}
schema {
query: Query
}
For these decorators to work, they need to be passed into the schema generator, which builds the schema from the AST. Once the schema is built completely, the decorators are applied to the thing they decorate — an argument, a field, a type, or the whole schema.
Behind the scenes, decorators are implemented as classes that fit the decorator interface: the decorator must be named, specify the locations in which it can be used, the arguments that it accepts, and it has a function that gets called when the decorator is applied.
When a decorator is applied, it modifies the schema to achieve the desired effect. For example, the +doc decorator can be used on types, fields and arguments, and it adds a description to the thing it’s applied to. The +log decorator can be applied to the schema, a type, or a field, and it wraps the resolve functions under its scope to add a call to a logger to it. Which logger instance is used can be configured in the constructor of the LogDecorator class.
Using decorator classes behind the scenes means that the decorators can be used with any GraphQL-JS schema, they don’t depend on the shorthand schema notation.
In essence, decorator classes are just a standard interface which returns a function that is then applied to the schema, so they could easily be implemented for GraphQL implementations in other languages.
In GraphQL-JS, this is roughly what decorators would look like:
import { applyDecorators } from 'graphql-tools';
import {
GraphQLSchema,
GraphQLString,
GraphQLObjectType,
} from 'graphql';
import { Doc, Log } from 'graphql-decorators';
const Logger = { log: (...args) => console.log(...args) };
const doc = new Doc();
const log = new Log({ logger: Logger });
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
decorators: [ log.apply({prefix: 'query' }) ],
fields: {
aString: {
type: GraphQLString,
decorators: [ doc.apply({ description: 'Returns hello world' }) ],
resolve: () => 'Hello World',
}
}
})
});
// call applyDecorators to apply the decorators to the schema
applyDecorators(schema);
Many decorators, such as validation decorators could be used by client libraries to do client-side checking.
For decorators to work in other languages, such as Python or Ruby, all that’s needed is an implementation of the same decorator in that language. Once enough decorators are implemented in different languages, your schema becomes completely portable. Imagine how cool it would be if you could easily switch your server from Node.js to Scala or the other way!
How can I use schema decorators?
Schema decorators are a proposal and not fully implemented yet. You can help to improve the spec and build a working implementation!
We’ve started implementing a first version of decorators in graphql-tools. So far there is only a prototype that lets you decorate types and fields in GraphQL-JS. We don’t yet have a parser for the schema language, but we hope that’s coming soon as well. You can either help build it or wait until it’s ready in to try it out.
It will still take some time to figure out in what form decorators (or even just metadata) should be added to the GraphQL spec and reference implementation. The way you can take action right now it is to join the discussion, sharing your thoughts on it or implementing some decorators of your own and share them with the rest of the community.
In the next few weeks I hope we can come up with a solid spec for decorators that makes them reusable across different languages and implementations. Personally, I’m planning to work on the schema parser and implement a few common and useful decorators to provide an example. My hope is that other members of the community will help improve the spec and implement more decorators to test / demonstrate the viability of the concept.
Here’s how you can participate right now:
- Join the discussion on GitHub
- Contribute to the spec / documentation
- Implement your own decorator and make a PR to showcase it!
Schema decorators were inspired by earlier work and ideas from Ville Immonen, Mikhail Novikov, Lee Byron, Igor Canadi, Clinton Wood and others in the following issues and PRs on GraphQL-JS: #114, #180, #264, #265 and #334.