GraphQL-Api
GraphQL-Api is an opinionated Graphql framework for Rails that supports auto generating queries based on Active Record models and plain Ruby objects.
Status
This project is an active work in progress. The API is currently being refined and subject to change. Contributions are more than welcome! Once the project hits a 1.0 release the API should be considered stable.
Example
Given the following model structure:
class Author < ActiveRecord::Base
has_many :blogs
# columns: name
end
class Blog < ActiveRecord::Base
belongs_to :author
# columns: title, content
end
GraphQL-Api will respond to the following queries for the blog resource:
query { blog(id: 1) { id, title, author { name } } }
query { blogs(limit: 5) { id, title, author { name } } }
mutation { createBlog(input: {name: "test", author_id: 2}) { blog { id } } }
mutation { updateBlog(input: {id: 1, title: "test"}) { blog { id } } }
mutation { deleteBlog(input: {id: 1}) { blog_id } }
GraphQL-Api also has support for command objects:
# Graphql mutation derived from the below command object:
# mutation { blogCreateCommand(input: {tags: ["test", "testing"], name: "hello"}) { blog { id, tags { name } } } }
class BlogCreateCommand < GraphQL::Api::CommandType
inputs name: :string, tags: [:string]
returns blog: Blog
def perform
# do something here to add some tags to a blog, you could also use ctx[:current_user] to access the user
{blog: blog}
end
end
... and query objects:
# Graphql query derived from the below query object:
# query { blogQuery(content_matches: ["name"]) { id, name } }
class BlogQuery < GraphQL::Api::QueryType
arguments name: :string, content_matches: [:string]
return_type [Blog]
def execute
Blog.all
end
end
Contents
Guides
Endpoint
Creating an endpoint for GraphQL-Api.
# inside an initializer or other file inside the load path
GraphSchema = GraphQL::Api::Schema.new.schema
# controllers/graphql_controller.rb
class GraphqlController < ApplicationController
# needed by the relay framework, defines the graphql schema
def index
render json: GraphSchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY)
end
# will respond to graphql requests and pass through the current user
def create
render json: GraphSchema.execute(
params[:query],
variables: params[:variables] || {},
context: {current_user: current_user}
)
end
end
Authorization
GraphQL-Api will check for an access_<field>?(ctx)
method on all model
objects before returning the value. If this method returns false, the
value will be nil
.
To scope queries for the model, define the graph_find(args, ctx)
and
graph_where(args, ctx)
methods using the ctx
parameter to get the
current user and apply a scoped query. For example:
class Blog < ActiveRecord::Base
belongs_to :author
def self.graph_find(args, ctx)
ctx[:current_user].blogs.find(args[:id])
end
def access_content?(ctx)
ctx[:current_user].is_admin?
end
end
For more complicated access management, define query objects and Poro's with only a subset of fields that can be accessed.
Documentation
Querying
Instantiate an instance of GraphQL-Api and get the schema
object which is
a GraphQL::Schema
instance from graphql-ruby.
graph = GraphQL::Api::Schema.new(commands: [], models: [], queries: [])
graph.schema.execute('query { ... }')
GraphQL-Api will load in all models, query objects and commands from the rails app directory automatically. If you store these in a different location you can pass them in directly to the new command.
Policy Objects
Policy objects can be created for controlling access to fields of a model. This enables you to provide a DRY solution for controlling access to granular attributes of models and PORO's.
The policy object is defined as follows, with the naming convention following
the pattern {Model}Policy
.
class UserPolicy < GraphQL::Api::Policy
def read?
# model is the instance, user is the current_user
user.role == 'admin' || user.id == model.id
end
end
There are four key methods that represent the CRUD operations:
read?
create?
update?
destroy?
These are checked by the default resolvers created by GraphQL-Api before the mutations or queries take place. Additionally, for control over access to fields, you may define a method on the policy with the following form:
access_{field}?
If this method exists, the policy will be consulted before reading the
field. You can control the response behaviour by overriding the
unauthorized_field_access(field_name)
method on the policy object.
By default it will return nil.
Model Objects
Model objects are the core return value from GraphQL-Api. They can be a plain old ruby object or they can be an active record model. Active record models have more automatic inference, whereas poro objects are more flexible.
Active Record
GraphQL-Api reads your associations and columns from your models and creates a graphql schema from them. In the examples above you can see that 'Author' is automatically accessible from the 'Blog' object because the belongs to relationship is set up. Column types are also inferred.
GraphQL-Api will set up two queries on the main Graphql query object. One for
a single record and another for a collection. You can override these queries
by setting a graph_find(args, ctx)
and graph_where(args, ctx)
class
methods on your model. The ctx
parameter will contain the context passed
in from the controller while the args
parameter will contain the arguments
passed into the graphql query.
Poro
Plain old ruby objects are supported by implementing a class method called
fields
on the object that returns the expected types hash.
Methods on the Poro should be defined with the same name as the provided
fields.
Command Objects
Command objects are an object oriented approach to defining mutations.
They take a set of inputs as well as a graphql context and provide a
perform
method that returns a Graphql understandable type. These objects
give you an object oriented abstraction for handling mutations.
Command objects must implement the interface defined in GraphQL::Api::CommandType
.
To better model controllers, you can define the commands actions
this
will allow a command to respond to multiple methods on the same class. For
example, the following code will model a restful controller using commands.
The mutation will be prefixed with the action name. For example, the code
below will create a updateBlogCommand
mutation as well as a deleteBlogCommand
.
class BlogCommand < GraphQL::Api::CommandType
inputs name: :string, tags: [:string], id: :integer
returns blog: Blog
# this tells GraphQL-Api to make two mutations that call the below methods.
actions :update, :delete
def update
blog = Blog.find(inputs[:id])
blog.update!(inputs.to_h)
{blog: blog}
end
def delete
blog = Blog.find(inputs[:id]).destroy!
{blog: blog}
end
end
Query Objects
Query objects are designed to provide a wrapper around complex queries with potentially a lot of inputs. They return a single type or array of types.
Query objects must implement the interface defined in GraphQL::Api::QueryType
Customization
Sometimes you cannot fit every possible use case into a library like GraphQL-Api as a result, you can always drop down to the excellent Graphql library for ruby to combine both hand rolled and GraphQL-Api graphql schemas. Here is an example creating a custom mutation.
simple_mutation = GraphQL::Relay::Mutation.define do
input_field :name, !types.String
return_field :item, types.String
resolve -> (inputs, ctx) { {item: 'hello'} }
end
graph = GraphQL::Api::Schema.new
mutation = graph.mutation do
field 'simpleMutation', simple_mutation.field
end
schema = GraphQL::Schema.define(query: graph.query, mutation: mutation)
puts schema.execute('mutation { simpleMutation(input: {name: "hello"}) { item } }')
The GraphQL::Api::Schema#mutation
and GraphQL::Api::Schema#query
methods accept
a block that allows you to add custom fields or methods to the mutation or
query definitions. You can refer to the graphql-ruby
docs for how to do this.
Types
Field types and argument types are all supplied as a hash of key value pairs. An exclamation mark at the end of the type marks it as required, and wrapping the type in an array marks it as a list of that type.
{
name: :string,
more_names: [:string],
required: :integer!,
}
The supported types are:
- integer
- text
- string
- decimal
- float
- boolean
Note, these are the same as active record's column types for consistency.
Roadmap
- [ ] Customizing resolvers
- [ ] Relay support
- [ ] Additional object support (enums, interfaces ...)
- [ ] Support non rails frameworks