DEV Community

Tayllan
Tayllan

Posted on

Gerenciando Múltiplos Schemas de GraphQL com Ruby on Rails

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Não esqueça de atualizar as rotas

Rails.application.routes.draw do
  post "/graphql", to: "public_graphql#execute"
  # ...
end

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 :) !

Discussion (0)