A sweet, extended DSL written on top of the graphql-ruby gem.
Looking for a quick overview of this gem in action? Head over to the Usage section.
This gem allows you to:
- Easily write object types and input types that are backed by ActiveRecord models.
- Automatically convert field names to snake_case.
- Automatically add
id
,createdAt
andupdatedAt
fields if these columns exist in your database schema. - Automatically determine the type of the field, based on your database schema and model validation rules, keeping things DRY.
- Easily write resolvers and mutators to encapsulate query and mutation logic.
- Provide an object-oriented layer, allowing easy refactoring of common code across queries and mutations.
- Look like (and function very similar to) Rails controllers, so that writing them is a breeze.
gem 'graphql'
gem 'graphql-sugar'
And then execute:
$ bundle
And finally, do some initial setup:
$ rails g graphql:sugar
This section provides a quick overview of the how simple the DSL can be, as well as a general workflow to follow:
Create the ObjectType:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
attribute :title
attribute :content
attribute :isPublic
relationship :user
relationship :comments
end
Create a Resolver:
class PostResolver < ApplicationResolver
parameter :id, !types.ID
def resolve
Post.find(params[:id])
end
end
Expose the Resolver:
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
resolver :post
end
Create the InputObjectType:
Inputs::PostInputType = GraphQL::InputObjectType.define do
name 'PostInput'
model_class Post
parameter :title
parameter :content
end
Create a Mutator:
class CreatePostMutator < ApplicationMutator
parameter :input, !Inputs::PostInputType
type !Types::PostType
def mutate
Post.create!(params[:input])
end
end
Expose the Mutator:
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'
mutator :createPost
end
Start by generating an ObjectType as you normally would:
$ rails g graphql:object Post
This would create the following under app/graphql/types/post_type.rb
:
Types::PostType = GraphQL::ObjectType.define do
name "Post"
end
Replace the name
line with a model_class
declaration:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
end
This automatically sets the name as PostType
. If you wish to overwrite the name, you can pass a second argument:
Types::PostType = GraphQL::ObjectType.define do
model_class Post, 'PostObject'
end
The model_class
declaration is required to use rest of the extended ObjectType DSL (like attributes
, attribute
, relationships
, relationship
, etc). If you forget to declare it however, a helpful exception is raised. 😄
Normally, this is how you would add a couple of fields to your ObjectType:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
field :id, !types.ID
field :title, !types.String
field :content, types.String
field :isPublic, !types.Boolean, property: :is_public
field :createdAt
field :updatedAt
end
However, using GraphQL::Sugar, you can now shorten this to:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
attribute :title
attribute :content
attribute :isPublic
end
Under the hood:
- The
id
,createdAt
andupdatedAt
fields are automatically added if your model has those attributes. - The type for the rest of the fields are automatically determined based on your
schema.rb
and model validations. (Read more about automatic type resolution.) - The fields automatically resolve to the snake_cased method names of the attribute name provided (eg.
isPublic
=>is_public
).
You can shorten this further active_model_serializers-style:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
attributes :title, :content, :isPublic
end
Or even more simply:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
attributes
end
... which automatically includes all the attributes of a model based on your schema. While NOT recommended for production, this provides easy scaffolding of model-backed object types during development.
Internally attribute
just defines a field
, but automatically determines the type and resolves to the model's snake_cased attribute. For simplicity, it follows the exact same syntax as field
, so you can override type or specify a resolve:
function:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
attribute :thumbnail, types.String, resolve: ->(obj, args, ctx) { obj.picture_url(:thumb) }
end
This is useful (and necessary) if you wish to expose attr_accessor
s defined in your model. (Read more about automatic type resolution.)
Side Note: You can always mix in good ol' field
s along with attribute
s if you really need to access the old DSL:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
attribute :title
field :isArchived, types.Boolean, resolve: ->(obj, args, ctx) { obj.is_archived? }
end
However, since the syntax is pretty much the same, it is preferable to use either field
or attribute
throughout the type definition for the sake of uniformity. You may have a non-model backed ObjectType for example, which can use field
s.
Assume the Post model has the following associations:
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
Normally, this is how you would define the relationship in your ObjectType:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
field :userId, !types.ID, property: :user_id
field :user, Types::UserType
field :comments, !types[Types::CommentType]
end
However, using GraphQL::Sugar, you can now shorten this to:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
relationship :user
relationship :comments
end
Under the hood:
- If the relationship is belongs_to, it automatically defines a field for the corresponding foreign key. It also determines the type and marks the association as non-null using automatic type resolution.
- If the relationship is has_one or has_many, it first looks for a corresponding Resolver (eg. in this case,
CommentsResolver
). If it doesn't find one, it defaults to calling method of the underlying association on the object (eg.obj.comments
)
You can shorten the above code to:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
relationships :user, :comments
end
Or even more simply:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
relationships
end
... which automatically reflects on all your model associations and includes them. While NOT recommended for production, this provides easy scaffolding of model-backed object types during development.
Side Note: Unlike attribute
, relationship
is not just syntactic sugar for field
and it does much more. It is recommended that you revert to using field
s (rather than attribute
) if you need to achieve a specific behavior involving associations. For example:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
relationship :user
field :recentComments, !types[Types::CommentType], resolve: ->(obj, args, ctx) {
obj.comments.not_flagged.recent.limit(3)
}
end
end
Your model attribute's type is automatically determined using Rails' reflection methods, as follows:
- First, we look at the column type:
:integer
gets mapped totypes.Int
(GraphQL::INT_TYPE
),:float
and:decimal
get mapped totypes.Float
(GraphQL::FLOAT_TYPE
),:boolean
gets mapped totypes.Boolean
(GraphQL::BOOLEAN_TYPE
),- and the rest get mapped to
types.String
(GraphQL::STRING_TYPE
).
- Then, we determine the non-nullability based on whether:
- You have specified
null: false
for the column in your schema, or - You have specified
presence: true
validation for the attribute in your model.
- You have specified
In instances where a type cannot be automatically determined, you must provide the type yourself. For example, attr_accessor
s are not persisted and don't have a corresponding column in your database schema.
Normally, this is how you would define your InputObjectType:
Inputs::PostInputType = GraphQL::InputObjectType.define do
name 'PostInput'
argument :title, types.String
argument :content, types.String
argument :isPublic, types.Boolean, as: :is_public
end
However, using GraphQL::Sugar, you can now shorten this to:
Inputs::PostInputType = GraphQL::InputObjectType.define do
name 'PostInput'
model_class 'Post'
parameter :title
parameter :content
parameter :isPublic
end
Under the hood,
parameter
uses the same automatic type resolution asattribute
, but creates arguments that are not-null by default. The default behavior passes all values to be validated in the model instead, in order to return proper error messages in the response. (TODO: Allow this behavior to be configured via an initializer.)- It allows sets the
:as
value to the snake_cased form of the provided name. (eg.:isPublic
=>:is_public
). This allows us to easily pass them into ActiveRecord'screate
andupdate_attributes
methods.
You can override the type to make a field non-null as follows:
Inputs::PostInputType = GraphQL::InputObjectType.define do
name 'PostInput'
model_class 'Post'
parameter :title, !types.String
parameter :content
end
In its simplest form, a Resolver simply inherits from ApplicationResolver
and contains a #resolve
method.
class PostsResolver < ApplicationResolver
def resolve
Post.all
end
end
To expose the resolver as a field, declare it in your root QueryType:
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
resolver :posts
end
To declare arguments, you can use the parameter
keyword which follows the same syntax:
class PostResolver < ApplicationResolver
parameter :id, !types.ID
def resolve
Post.find(params[:id])
end
end
The benefit is that all parameter
s (read: arguments) are loaded into a params
object, with all keys transformed into snake_case. This allows them to be easily used with ActiveRecord methods like where
and find_by
.
You also have object
and context
available in your resolve method:
class PostsResolver < ApplicationResolver
def resolve
(object || context[:current_user]).posts
end
end
Assume the following GraphQL query ("fetch 10 posts, along with the authors and 2 of their highest rated posts."):
query {
posts(limit: 10) {
title
content
user {
name
posts(limit: 2, sort: "rating_desc") {
title
rating
}
}
}
}
When executed, we resolve both the first and second posts
using PostsResolver
. This means:
-
All the
argument
s (orparameter
s) available to your top levelposts
are available to all your nestedposts
s through relationships without any extra work. -
The
object
value passed to yourPostsResolver#resolve
function is very important. This would be a good place to perform an authorization check to see if the current user has access to this relationship on theobject
.
A quick detour: At the top of your graph, you have your root_value (read more), which the graphql-ruby library allows you to set for your schema. By default, this is null
. You can either explicitly set this root_value, or implicitly consider to be the current user (or current organization, or whatever your application deems it to be).
For example,
class PostsResolver < ApplicationResolver
def resolve
parent_object = (object || context[:current_user])
authorize! :view_posts, parent_object
parent_object.posts
end
end
In its simplest form, a Mutator simply inherits from ApplicationMutator
and contains a #mutate
method:
class CreatePostMutator < ApplicationMutator
parameter :input, !Inputs::PostInputType
type !Types::PostType
def mutate
Post.create!(params[:input])
end
end
To expose the mutator as a field, declare it in your root MutationType:
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'
mutator :createPost
end
Just like resolvers, you have access to object
, params
and context
:
class UpdatePostMutator < ApplicationMutator
parameter :id, !types.ID
parameter :input, !Inputs::PostInputType
type !Types::PostType
def mutate
post = context[:current_user].posts.find(params[:id])
post.update_attributes!(params[:input])
post
end
end
When you install the gem using rails g graphql:sugar
, it creates the following files:
app/graphql/functions/application_function.rb
app/graphql/resolvers/application_resolver.rb
app/graphql/mutators/application_mutator.rb
All your resolvers inherit from ApplicationResolver
and all your mutators inherit from ApplicationMutator
, both of which in turn inherit from ApplicationFunction
. You can use these classes to write shared code common to multiple queries, mutations, or both.
Pagination and Sorting: You can easily create methods that enable common features.
class ApplicationResolver < ApplicationFunction
include GraphQL::Sugar::Resolver
def self.sortable
parameter :sort, types.String
parameter :sortDir, types.String
end
end
Use in your other resolvers:
class PostsResolver < ApplicationResolver
sortable
def resolve
# ...
end
end
Shared Code: You can also easily share common code across a specific set of mutators. For example, your CreatePostMutator
and UpdatePostMutator
could inherit from PostMutator
, which inherits from ApplicationMutator
.
In a large app, you can quite easily end up with tons of mutations. During setup, GraphQL::Sugar adds a few lines to your eager_load_paths so you can group them in folders, while maintaining mutations at the root level. For example,
# Folder Structure
app/graphql/mutators/
- posts
- create_post_mutator.rb
- update_post_mutator.rb
- users
- create_user_mutator.rb
- update_user_mutator.rb
- application_mutator.rb
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'
mutator :createPost
mutator :updatePost
mutator :createUser
mutator :updateUser
end
A few basic generators have been written to quickly create some of the boilerplate code. They may not work perfectly, and the generated code may require further editing.
$ rails g graphql:resolver BlogPosts
Creates a BlogPostsResolver
class at app/graphql/resolvers/blog_posts_resolver.rb
.
$ rails g graphql:mutator CreateBlogPost
Creates a CreateBlogPostMutator
class under app/graphql/mutators/create_blog_post_mutator.rb
.
Many thanks to the work done by the authors of the following gems, which this gem uses as a foundation and/or inspiration:
Maintained and sponsored by KeepWorks.
Bug reports and pull requests are welcome on GitHub at https://github.com/keepworks/graphql-sugar. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.