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,createdAtandupdatedAtfields 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
endCreate a Resolver:
class PostResolver < ApplicationResolver
parameter :id, !types.ID
def resolve
Post.find(params[:id])
end
endExpose the Resolver:
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
resolver :post
endCreate the InputObjectType:
Inputs::PostInputType = GraphQL::InputObjectType.define do
name 'PostInput'
model_class Post
parameter :title
parameter :content
endCreate a Mutator:
class CreatePostMutator < ApplicationMutator
parameter :input, !Inputs::PostInputType
type !Types::PostType
def mutate
Post.create!(params[:input])
end
endExpose the Mutator:
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'
mutator :createPost
endStart 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"
endReplace the name line with a model_class declaration:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
endThis 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'
endThe 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
endHowever, using GraphQL::Sugar, you can now shorten this to:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
attribute :title
attribute :content
attribute :isPublic
endUnder the hood:
- The
id,createdAtandupdatedAtfields are automatically added if your model has those attributes. - The type for the rest of the fields are automatically determined based on your
schema.rband 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
endOr 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) }
endThis is useful (and necessary) if you wish to expose attr_accessors defined in your model. (Read more about automatic type resolution.)
Side Note: You can always mix in good ol' fields along with attributes 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? }
endHowever, 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 fields.
Assume the Post model has the following associations:
class Post < ApplicationRecord
belongs_to :user
has_many :comments
endNormally, 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]
endHowever, using GraphQL::Sugar, you can now shorten this to:
Types::PostType = GraphQL::ObjectType.define do
model_class Post
relationship :user
relationship :comments
endUnder 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
endOr 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 fields (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
endYour model attribute's type is automatically determined using Rails' reflection methods, as follows:
- First, we look at the column type:
:integergets mapped totypes.Int(GraphQL::INT_TYPE),:floatand:decimalget mapped totypes.Float(GraphQL::FLOAT_TYPE),:booleangets 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: falsefor the column in your schema, or - You have specified
presence: truevalidation 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_accessors 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
endHowever, 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
endUnder the hood,
parameteruses 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
:asvalue to the snake_cased form of the provided name. (eg.:isPublic=>:is_public). This allows us to easily pass them into ActiveRecord'screateandupdate_attributesmethods.
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
endIn its simplest form, a Resolver simply inherits from ApplicationResolver and contains a #resolve method.
class PostsResolver < ApplicationResolver
def resolve
Post.all
end
endTo expose the resolver as a field, declare it in your root QueryType:
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
resolver :posts
endTo 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
endThe benefit is that all parameters (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
endAssume 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
arguments (orparameters) available to your top levelpostsare available to all your nestedpostss through relationships without any extra work. -
The
objectvalue passed to yourPostsResolver#resolvefunction 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
endIn 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
endTo expose the mutator as a field, declare it in your root MutationType:
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'
mutator :createPost
endJust 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
endWhen 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
endUse in your other resolvers:
class PostsResolver < ApplicationResolver
sortable
def resolve
# ...
end
endShared 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
endA 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.