Nesse post quero mostrar uma forma de gerenciar schemas de GraphQL no Ruby on Rails, entretanto, boa parte também poderá ser aplicada a qualquer outra aplicação que use a gem graphql.
Um dos motivos para se separar Schemas GraphQL é quando há recursos que necessitam ser acessados com autenticação e outro sem autenticação. Você também pode ter a necessidade de separar os schemas por aplicação que será fornecida, como a que é entregue a um aplicativo e a entregue a um sistema interno administrativo da aplicação. Assim, seus schemas entregam apenas o que é necessário para a aplicação que irá consumir e também restringe o acesso de recursos internos.
Hands On!
Todo o código fonte pode ser encontrado aqui
Neste exemplo, vamos fazer uma API de um blog, mas para manter a simplicidade, vamos ignorar a parte de autenticação.
Teremos dois schemas.
- Schema público que trará a lista de posts e seus respectivos autores.
- Schema privado que permitirá cadastrar novos posts.
Após criar a aplicação, instalar a gem graphql e executar o comando rails generate graphql:install
, teremos a seguinte estrutura de arquivos.
app
├── controllers
│ ├── application_controller.rb
│ └── graphql_controller.rb
└── graphql
├── app_name_schema.rb
├── mutations
│ └── base_mutation.rb
└── types
├── base_argument.rb
├── base_connection.rb
├── base_edge.rb
├── base_enum.rb
├── base_field.rb
├── base_input_object.rb
├── base_interface.rb
├── base_object.rb
├── base_scalar.rb
├── base_union.rb
├── mutation_type.rb
├── node_type.rb
└── query_type.rb
O que precisamos é a pasta graphql em namespaces, cada um com suas Queries, Mutations e Types.
Veja como é o schema inicial gerado pelo instalador.
# app/graphql/app_name_schema.rb
class AppNameSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
end
Um schema é composto basicamente por uma classe que diz quais são as mutations e uma class que define quais são as queries.
Veja como é o controller inicial gerado pelo instalador.
class GraphqlController < ApplicationController
protect_from_forgery with: :null_session
def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
# Query context goes here, for example:
# current_user: current_user,
}
result = AppNameSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue StandardError => e
raise e unless Rails.env.development?
handle_error_in_development(e)
end
private
# Handle variables in form data, JSON body, or a blank value
def prepare_variables(variables_param)
case variables_param
when String
if variables_param.present?
JSON.parse(variables_param) || {}
else
{}
end
when Hash
variables_param
when ActionController::Parameters
variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{variables_param}"
end
end
def handle_error_in_development(e)
logger.error e.message
logger.error e.backtrace.join("\n")
render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
end
end
Precisaremos definir dois endpoints graphql, um para o schema público e outro para o schema privado.
Mas primeiro, vamos gerar os models que usaremos na aplicação.
rails g model author name
rails g model post title content:text author:references
rails g model email email
O model de email será usado para o público assinar o recebimento de posts por email.
Vamos começar pelo graphql público. Vamos ter 1 query
que lista os posts, 1 query
que retorna um post pelo ID e uma mutation
que cadastra o email na lista de emails.
A pasta do graphql público fica assim.
# app/graphql/public_graphql.rb
class PublicGraphql < GraphQL::Schema
mutation(Mutations)
query(Queries)
end
# app/graphql/public_graphql/mutations.rb
class PublicGraphql
class Mutations < Types::BaseObject
field :add_email_to_list, mutation: AddEmailToList
end
end
# app/graphql/public_graphql/queries.rb
class PublicGraphql
class Queries < Types::BaseObject
include Posts
include PostById
end
end
Acima já descrevi as queries e mutations que teremos, elas ficarão dentro dos namespaces Queries e Mutations.
A seguir, as Queries.
# app/graphql/public_graphql/queries/posts.rb
class PublicGraphql
class Queries
module Posts
extend ActiveSupport::Concern
included do
field :posts, [Types::Post], null: false
end
def posts
Post.all
end
end
end
end
# app/graphql/public_graphql/queries/post_by_id.rb
class PublicGraphql
class Queries
module PostById
extend ActiveSupport::Concern
included do
field :post_by_id, Types::Post, null: true do
argument :id, GraphQL::Types::ID, required: true
end
end
def post_by_id(id:)
Post.find_by(id:)
end
end
end
end
Para que as queries funcionem, precisamos dos types que utilizamos nelas, que é apenas o Post
.
# app/graphql/types/post.rb
module Types
class Post < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :content, String, null: false
field :author, Types::Author, null: false
end
end
# app/graphql/types/author.rb
module Types
class Author < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
end
end
Note que colocamos os types na pasta raiz Types mesmo. Fiz isso pois precisamos desses tipos no graphql privado também, então aqui fica compartilhado. Caso o tipo seja de uso somente para aquele schema em especifico, você pode criar o tipo dentro do namespace PublicGraphql mesmo.
Já as mutations, movi a classe Mutations::BaseMutation
(essa é a que foi gerada automaticamente pela gem) para Types::BaseMutation
. Assim a pasta de mutations pode ser excluída.
A mutation AddEmailToList fica assim:
# app/graphql/public_graphql/mutations/add_email_to_list.rb
class PublicGraphql
class Mutations
class AddEmailToList < Types::BaseMutation
argument :email, String, required: true
field :success, Boolean, null: false
def resolve(email:)
{ success: !!Email.create(email:) }
end
end
end
end
Para finalizar o Graphql público, falta seu controller. Assim, criamos o PublicGraphqlController
# app/controllers/public_graphql_controller
class PublicGraphqlController < ApplicationController
def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
result = PublicGraphql.execute(query, variables:, operation_name:) # here we call the PublicGraphql class
render json: result
rescue StandardError => e
raise e unless Rails.env.development?
handle_error_in_development(e)
end
private
# same private methods from the generated class
end
Não esqueça de atualizar as rotas
Rails.application.routes.draw do
post "/graphql", to: "public_graphql#execute"
# ...
end
O PrivateGraphql podemos seguir o mesmo estilo. Aqui vamos ter a listagem de posts de um autor e o cadastro de posts.
# app/graphql/private_graphql.rb
class PrivateGraphql < GraphQL::Schema
mutation(Mutations)
query(Queries)
end
# app/graphql/private_graphql/queries.rb
class PrivateGraphql
class Queries < Types::BaseObject
include MyPosts
end
end
# app/graphql/private_graphql/mutations.rb
class PrivateGraphql
class Mutations < Types::BaseObject
field :create_post, mutation: CreatePost
end
end
# app/graphql/private_graphql/queries/my_posts.rb
class PrivateGraphql
class Queries
module MyPosts
extend ActiveSupport::Concern
included do
field :my_posts, [Types::Post], null: false
end
def my_posts
context[:current_author].posts
end
end
end
end
# app/graphql/private_graphql/mutations/create_post.rb
class PrivateGraphql
class Mutations
class CreatePost < Types::BaseMutation
argument :title, String, required: true
argument :content, String, required: true
field :post, Types::Post, null: false
def resolve(title:, content:)
post = Post.new(title:, content:, author: context[:current_author])
if post.save
{post:}
else
{}
end
end
end
end
end
# app/controllers/private_graphql_controller.rb
class PrivateGraphqlController < ApplicationController
# here you can put an authentication method to protect your private graphql schema
# before_action :authenticate!
def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = { current_author: Author.first } # hardcoding an author just for example
result = PrivateGraphql.execute(query, variables:, context:, operation_name:)
render json: result
rescue StandardError => e
raise e unless Rails.env.development?
handle_error_in_development(e)
end
private
# same private methods from the generated class
end
# config/routes.rb
Rails.application.routes.draw do
# ...
post "/private_graphql", to: "private_graphql#execute" # added
end
E pronto! Temos os dois schemas bem separados e organizados.
Nossa nova estrutura ficou da seguinte forma.
├── private_graphql
│ ├── mutations
│ │ └── create_post.rb
│ ├── mutations.rb
│ ├── queries
│ │ └── my_posts.rb
│ └── queries.rb
├── private_graphql.rb
├── public_graphql
│ ├── mutations
│ │ └── add_email_to_list.rb
│ ├── mutations.rb
│ ├── queries
│ │ ├── post_by_id.rb
│ │ └── posts.rb
│ └── queries.rb
├── public_graphql.rb
└── types
├── author.rb
├── base_argument.rb
├── base_connection.rb
├── base_edge.rb
├── base_enum.rb
├── base_field.rb
├── base_input_object.rb
├── base_interface.rb
├── base_mutation.rb
├── base_object.rb
├── base_scalar.rb
├── base_union.rb
├── mutation_type.rb
├── node_type.rb
├── post.rb
└── query_type.rb
Nessa organização fica simples encontrar onde cada operação está e só de bater o olho na pasta você consegue ver tudo o que aquela API expõe.
Vale notar também que separar cada query e cada mutation em um único arquivo também ajuda na hora dos testes, pois eles ficam mais concisos!
O que acha dessa organização? Como você organiza múltiplos schemas graphql? Deixa teu comentário abaixo :) !
Top comments (0)