Gerenciando Múltiplos Schemas de GraphQL com Ruby on Rails
Tags: ruby, rails, graphql
Published: 2022-05-22 12:12:50

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](https://graphql-ruby.org/).

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](https://github.com/mtayllan/graphql-schemas)

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.
```ruby
# 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.
```ruby
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.

```ruby
# 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.

```ruby
# 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`.

```ruby
# 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:
```ruby
# 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

```ruby
# 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
```ruby
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. 

```ruby
# 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 :) !