Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/keepworks/graphql-sugar
A sweet, extended DSL written on top of the graphql-ruby gem.
https://github.com/keepworks/graphql-sugar
graphql graphql-ruby rails ruby
Last synced: 3 months ago
JSON representation
A sweet, extended DSL written on top of the graphql-ruby gem.
- Host: GitHub
- URL: https://github.com/keepworks/graphql-sugar
- Owner: keepworks
- License: mit
- Created: 2017-10-25T20:52:42.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2023-02-17T06:10:13.000Z (over 1 year ago)
- Last Synced: 2024-07-26T16:16:14.244Z (4 months ago)
- Topics: graphql, graphql-ruby, rails, ruby
- Language: Ruby
- Size: 33.2 KB
- Stars: 42
- Watchers: 3
- Forks: 10
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# GraphQL::Sugar
A sweet, extended DSL written on top of the [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) gem.
**Looking for a quick overview of this gem in action?** Head over to the [Usage](#usage) section.
This gem allows you to:
* Easily write [object types](#object-types) and [input types](#input-types) that are backed by ActiveRecord models.
* Automatically convert field names to snake_case.
* Automatically add `id`, `createdAt` and `updatedAt` 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](#resolvers) and [mutators](#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.## Installation
```ruby
gem 'graphql'
gem 'graphql-sugar'
```And then execute:
$ bundle
And finally, do some initial setup:
$ rails g graphql:sugar
## Usage
This section provides a quick overview of the how simple the DSL can be, as well as a general workflow to follow:
### Writing Queries
Create the ObjectType:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postattribute :title
attribute :content
attribute :isPublicrelationship :user
relationship :comments
end
```Create a [Resolver](#resolvers):
```ruby
class PostResolver < ApplicationResolver
parameter :id, !types.IDdef resolve
Post.find(params[:id])
end
end
```Expose the Resolver:
```ruby
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'resolver :post
end
```### Writing Mutations
Create the InputObjectType:
```ruby
Inputs::PostInputType = GraphQL::InputObjectType.define do
name 'PostInput'model_class Post
parameter :title
parameter :content
end
```Create a [Mutator](#mutators):
```ruby
class CreatePostMutator < ApplicationMutator
parameter :input, !Inputs::PostInputTypetype !Types::PostType
def mutate
Post.create!(params[:input])
end
end
```Expose the Mutator:
```ruby
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'mutator :createPost
end
```## Usage
### Object Types
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`:
```ruby
Types::PostType = GraphQL::ObjectType.define do
name "Post"
end
```Replace the `name` line with a `model_class` declaration:
```ruby
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:
```ruby
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. :smile:
#### Defining attributes
*Normally*, this is how you would add a couple of fields to your ObjectType:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postfield :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:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postattribute :title
attribute :content
attribute :isPublic
end
```Under the hood:
* The `id`, `createdAt` and `updatedAt` 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](#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](https://github.com/rails-api/active_model_serializers)-style:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postattributes :title, :content, :isPublic
end
```Or even more simply:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postattributes
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:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postattribute :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](#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:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postattribute :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.
#### Defining relationships
Assume the Post model has the following associations:
```ruby
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
```*Normally*, this is how you would define the relationship in your ObjectType:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postfield :userId, !types.ID, property: :user_id
field :user, Types::UserTypefield :comments, !types[Types::CommentType]
end
```However, using GraphQL::Sugar, you can now shorten this to:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postrelationship :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](#automatic-type-resolution).
* If the relationship is **has_one** or **has_many**, it first looks for a corresponding [Resolver](#resolvers) (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:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postrelationships :user, :comments
end
```Or even more simply:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postrelationships
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:
```ruby
Types::PostType = GraphQL::ObjectType.define do
model_class Postrelationship :user
field :recentComments, !types[Types::CommentType], resolve: ->(obj, args, ctx) {
obj.comments.not_flagged.recent.limit(3)
}
end
end
```#### Automatic Type Resolution
Your model attribute's type is automatically determined using Rails' reflection methods, as follows:
* First, we look at the column type:
* `:integer` gets mapped to `types.Int` (`GraphQL::INT_TYPE`),
* `:float` and `:decimal` get mapped to `types.Float` (`GraphQL::FLOAT_TYPE`),
* `:boolean` gets mapped to `types.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.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.
### Input Types
*Normally*, this is how you would define your InputObjectType:
```ruby
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:
```ruby
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](#automatic-type-resolution) as `attribute`, 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's `create` and `update_attributes` methods.You can override the type to make a field non-null as follows:
```ruby
Inputs::PostInputType = GraphQL::InputObjectType.define do
name 'PostInput'model_class 'Post'
parameter :title, !types.String
parameter :content
end
```### Resolvers
In its simplest form, a Resolver simply inherits from `ApplicationResolver` and contains a `#resolve` method.
```ruby
class PostsResolver < ApplicationResolver
def resolve
Post.all
end
end
```To expose the resolver as a field, declare it in your root QueryType:
```ruby
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:
```ruby
class PostResolver < ApplicationResolver
parameter :id, !types.IDdef 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:
```ruby
class PostsResolver < ApplicationResolver
def resolve
(object || context[:current_user]).posts
end
end
```#### Thinking in Graphs *using Resolvers*
Assume the following GraphQL query ("fetch 10 posts, along with the authors and 2 of their highest rated posts."):
```
query {
posts(limit: 10) {
title
contentuser {
nameposts(limit: 2, sort: "rating_desc") {
title
rating
}
}
}
}
```When executed, we resolve both the first and second `posts` using `PostsResolver`. This means:
1. All the `argument`s (or `parameter`s) available to your top level `posts` are available to all your nested `posts`s through relationships without any extra work.
2. The `object` value passed to your `PostsResolver#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 the `object`.
**A quick detour:** At the top of your graph, you have your **root_value** ([read more](http://graphql-ruby.org/queries/executing_queries.html#root-value)), which the [graphql-ruby](https://github.com/rmosolgo/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,
```ruby
class PostsResolver < ApplicationResolver
def resolve
parent_object = (object || context[:current_user])
authorize! :view_posts, parent_objectparent_object.posts
end
end
```### Mutators
In its simplest form, a Mutator simply inherits from `ApplicationMutator` and contains a `#mutate` method:
```ruby
class CreatePostMutator < ApplicationMutator
parameter :input, !Inputs::PostInputTypetype !Types::PostType
def mutate
Post.create!(params[:input])
end
end
```To expose the mutator as a field, declare it in your root MutationType:
```ruby
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'mutator :createPost
end
```Just like resolvers, you have access to `object`, `params` and `context`:
```ruby
class UpdatePostMutator < ApplicationMutator
parameter :id, !types.ID
parameter :input, !Inputs::PostInputTypetype !Types::PostType
def mutate
post = context[:current_user].posts.find(params[:id])
post.update_attributes!(params[:input])
post
end
end
```### Organizing Your Code
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.
#### Applying OO principles
*Pagination and Sorting:* You can easily create methods that enable common features.
```ruby
class ApplicationResolver < ApplicationFunction
include GraphQL::Sugar::Resolverdef self.sortable
parameter :sort, types.String
parameter :sortDir, types.String
end
end
```Use in your other resolvers:
```ruby
class PostsResolver < ApplicationResolver
sortabledef 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`.
#### Tips for Large Applications
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
``````ruby
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'mutator :createPost
mutator :updatePostmutator :createUser
mutator :updateUser
end
```### Generators
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`.
## Credits
Many thanks to the work done by the authors of the following gems, which this gem uses as a foundation and/or inspiration:
- [graphql-ruby](https://github.com/rmosolgo/graphql-ruby)
- [graphql-activerecord](https://github.com/goco-inc/graphql-activerecord)
- [graphql-rails-resolver](https://github.com/colepatrickturner/graphql-rails-resolver)
- [active_model_serializers](https://github.com/rails-api/active_model_serializers)---
Maintained and sponsored by [KeepWorks](http://www.keepworks.com).
![KeepWorks](http://www.keepworks.com/assets/logo-800bbf55fabb3427537cf669dc8cd018.png "KeepWorks")
## Contributing
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](http://contributor-covenant.org) code of conduct.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).