GraphQL + Rails na prática – Inputs customizados

No último posts da série GraphQL + Rails na prática vamos ver como criar tipos específicos para inputs que facilitam o reuso do código.

Voltando no arquivo que define as mutações (‘app/graphql/types/mutation_type.rb’) temos a seguinte definição de tipo para criar um novo jogo:

# app/graphql/types/mutation_type.rb
field :new_game do
  description 'Creates a new game'
  type Types::GameType
  argument :name, !types.String
  argument :launch_year, !types.String
  argument :description, types.String

  resolve -> (root, args, _ctx) do
    Game.create(name: args[:name],
                launch_year: args[:launch_year],
                description: args[:description])
  end
end

Imagine que os campos ‘name’, ‘launch_year’ e ‘description’ vão aparecer juntos em vários outros lugares podemos evitar duplicação criando um ‘InputObjectType’. A principal diferença do ‘ObjectType’ que usamos antes é que esses tipos podem ser utilizados como inputs, ao invés de retorno de consultas.

Criando inputs customizados

Crie uma nova pasta ‘app/graphql/inputs/’ e adicione um arquivo ‘game_input.rb’, nele vamos definir o novo tipo, agregando todos aqueles campos.

# app/graphql/inputs/game_input.rb
Inputs::GameInput = GraphQL::InputObjectType.define do
  name 'GameInput'
  description 'Game attributes'

  argument :name, !types.String
  argument :launch_year, !types.String
  argument :description, types.String
end

Agora basta utilizar esse novo tipo na mutação que cria um jogo:

# app/graphql/types/mutation_type.rb
field :new_game do
  description 'Creates a new game'
  type Types::GameType
  argument :game, Inputs::GameInput

  resolve -> (root, args, _ctx) do
    Game.create(name: args[:game][:name],
    launch_year: args[:game][:launch_year],
    description: args[:game][:description])
  end
end

Vale lembrar que essa modificação altera a forma como as consultas são feitas! A nova consulta deve ser assim:

mutation CreateGame {
  new_game(game:{name: "The Legend of Zelda", launch_year: "1986"}) {
    name
    launch_year
    description
  }
}

Se você quiser evitar uma mudança destrutiva na sua API uma ideia é criar uma nova mutação que usa o tipo input, assim quem ainda precisar da consulta antiga consegue utilizá-la.

Podemos dar ainda mais uma passo e simplificar o ‘resolver’ da mutação que cria jogos aproveitando que, ao criar novas entradas com o ActiveRecord, é possível utilizar um hash com os parâmetros. Então o ‘resolver’ da mutação ‘new_game’ pode ficar com apenas uma linha:

# app/graphql/types/mutation_type.rb
field :new_game do
  description 'Creates a new game'
  type Types::GameType
  argument :game, Inputs::GameInput

  resolve -> (root, args, _ctx) do
    Game.create(args[:game].to_h)
  end
end

Com isso finalizamos o nosso tour de GraphQL + Rails com a mão na massa! Ainda tem muitas coisas legais que podem ser exploradas, como filtragem e paginação em consultas, autenticação e autorização na API, balanceamento de carga, consumir APIs GraphQL etc. Quem sabe numa próxima série de posts? 🙂

Se você quiser conhecer mais sobre GraphQL o site https://www.howtographql.com/ contém muita informação legal além de tutoriais em várias linguagens, então super recomendo a leitura.

Se você curtiu essa série me avisa lá no twitter e compartilha os posts com a galera que tá querendo colocar a mão na massa em GraphQL!

GraphQL + Rails na prática – Separando lógica de negócio

Assim como é uma boa prática manter o Controller livre de lógica em aplicações REST, no mundo GraphQL é importante seguir a mesma ideia e evitar colocar muito código junto com a definição de tipos e campos. Assim você consegue uma maior facilidade de manutenção, desde testes até desacoplamento e reuso.

O que fazer quando não temos Games

Se voltarmos no nosso campo ‘game’ definido em ‘query_type.rb’ vemos que a lógica atual apenas busca por um Game mas não lida com a falta de dados e outras exceções que podem aparecer:


# app/graphql/types/query_type.rb
field :game do
  type Types::GameType
  argument :id, !types.Int
  resolve -> (root, args, ctx) {
    Game.find(args[:id])
  }
end

Caso o ‘id’ passado não corresponda a nenhum dado no banco uma exceção será lançada e a requisição será interrompida. A maneira mais simples de resolver esse cenário é utilizar o método ‘find_by_’ do Rails que não lança uma exceção caso o dado não seja encontrado, mas ainda continuamos retornando um ‘null’.

Se você quiser saber mais sobre por que retornar null é uma má ideia, dá uma olhada nesse post: https://brizeno.wordpress.com/2016/06/21/refatorando-tudo-nao-retorne-nulo-nunca/.

Uma solução, ao invés de retornar nulo, é utilizar um objeto que é funcionalmente válido mas não possui nenhuma informação. Seguindo essa ideia podemos criar um método no nosso model ‘Game’ que cria um “objeto nulo”.

#app/models/game.rb
class Game < ApplicationRecord
  def self.nil_game
    Game.new(name: '',
             description: '',
launch_year: 0,
             characters: [])
  end
end

O nosso objeto nulo é um objeto válido, mas que não tem nenhuma informação, assim ao invés de retornar um nulo, que precisa ser tratado de maneira diferente, temos uma resposta que pode ser tratada como qualquer outra.

Criando um Resolver

Agora precisamos adicionar o tratamento de, quando a busca pelo game retornar um nulo, utilizar o objeto nulo. Colocar essa lógica lá na definição de tipo do GraphQL pode não ser uma boa ideia, então temos duas opções: 1) criar um método no modelo que faz isso e 2) criar um Resolver para lidar com essa lógica.

Qual opção faz mais sentido depende do seu contexto, mas para esse post vamos explorar como criar um Resolver para separar a lógica da definição do tipo. Crie uma nova pasta dentro de ‘app/graphql’ chamada ‘resolvers’ e adicione o arquivo ‘find_game.rb’. Nele vamos colocar o seguinte código:

# app/graphql/resolvers/find_game.rb
class Resolver::FindGame < GraphQL::Function
  argument :id, !types.Int
  type Types::GameType

  def call(root, args, ctx) do
    game = Game.find_by_id(args[:id])
    game.nil? ? Game.nil_game : game
  end
end

A estrutura interna do resolver é bem parecida com a definição do tipo, a grande diferença é que usamos o método 'call' para conter a lógica.

Agora, lá na definição do tipo em 'app/graphql/types/query_type.rb' modificamos o campo 'game' para o seguinte:

# app/graphql/types/query_type.rb
field :game, function: Resolvers::FindGame.new

Agora, conseguimos executar todas as queries de maneira um pouco mais segura.

Criar Resolvers dedicados é uma boa ideia quando temos muita lógica de negócio pois eles são fáceis de testar (basta instanciar o resolver e chamar 'call'). Outra boa refatoração que pode ser feita é extrair toda aquela lógica que está no 'mutation_type.rb' para um resolver dedicado.

Nos próximos posts vamos explorar como criar tipos de inputs para reduzir a duplicação nas definições de tipos. Então acompanhem os próximos posts e compartilha essa série de tutoriais com as pessoas que você acha que gostariam de aprender mais sobre GraphQL!

GraphQL + Rails na prática – Mutations

Nos posts anteriores vimos como podemos expor dados de maneira bem simples e flexível com GraphQL, agora vamos ver como podemos criar e modificar dados seguindo a mesma ideia de grafos.

Criando novos jogos

Se voltarmos no arquivo que define o nó inicial do nosso grafo (‘app/graphql/graphql_games_schema.rb’) vamos ver que tem o código que define ‘query(Types::QueryType)’, onde fizemos as últimas mudanças, e também define ‘mutation(Types::MutationType)’:

# app/graphql/types/query_type.rb
GraphqlGamesSchema = GraphQL::Schema.define do
  mutation(Types::MutationType)
  query(Types::QueryType)
end

Mutations são campos que modificam os dados dos nossos recursos, não existe uma diferença prática a ideia é mais separar conceitualmente as consultas que produzem efeitos colaterais (como atualizar, criar ou remover informações).

Vamos criar uma mutação no arquivo ‘app/graphql/types/mutation_type.rb’ que foi gerado quando instalamos GraphQL na nossa aplicação. O código é bem parecido com os outros, criamos os campos, definimos argumentos e o código do ‘resolve’ vai executar nossa lógica.

# app/graphql/types/mutation_type.rb

Types::MutationType = GraphQL::ObjectType.define do
  field :new_game do
    description 'Creates a new game'
    type Types::GameType
    argument :name, !types.String
    argument :launch_year, !types.String
    argument :description, types.String

    resolve -> (root, args, _ctx) do
      Game.create(name: args[:name],
                  launch_year: args[:launch_year],
                  description: args[:description])
    end
  end
end

Lembre que quando definimos o ‘GameType’ os campos ‘name’ e ‘launch_year’ eram obrigatórios, então precisamos também sinalizar esses atributos com o ‘!types’.

Podemos executar essa consulta no GraphiQL App da seguinte maneira:

mutation CreateGame {
  new_game(name: "The Legend of Zelda", launch_year: "1986") {
    name
    launch_year
    description
  }
}

Ao criarmos um novo jogo executamos a query da mesma forma como em qualquer outro campo. A consulta anterior vai criar um novo jogo e retornar o nome, ano de lançamento e a descrição.

Adicionando personagens aos jogos

Não adicionamos o campo para as personagens do jogo pois os o ‘GameType’ não pode ser utilizado para inputs via argumentos. Por padrão apenas os tipos escalares podem ser argumentos, vamos ver como criar input types no próximo post.

Uma maneira de contornar o problema é passar os ids das personagens que devem ser adicionadas. Vamos criar um nova mutation que busca um jogo e adiciona personagens a esse jogo:

# app/graphql/types/mutation_type.rb
field :add_char_to_game do
  description 'Adds a character to a game'
  type Types::GameType
  argument :game_id, !types.Int
  argument :characters_id, !types[types.Int]

  resolve -> (root, args, _ctx) do
    game = Game.find(args[:game_id])
    args[:characters_id].each do |id|
      char = Character.find(id)
      game.characters << char unless game.characters.include?(char)
    end
    game.save
    game
  end
end

No 'resolve' desse campo vamos adicionar cada um dos ids passados no 'args[:characters_id]' e adicionar a personagem ao jogo caso já não esteja lá. No final salvamos e retornamos o jogo.

Podemos usar essa consulta pra adicionar personagens aos jogos:

mutation AddCharToGame {
  add_char_to_game(game_id: 2, characters_id: [4]) {
    name
    characters {
      name
    }
  }
}

Seguindo a mesma ideia podemos criar um mutation que remove jogo, basta passar o id do jogo e no 'resolve' remover aquela entrada. Com isso, conseguimos seguir todo o fluxo de CRUD (Create, Read, Update e Delete) com o GraphQL.

No entanto o nosso exemplo está bem simples, não lidamos com erros e nem imprevistos, além disso toda a lógica da aplicação vive nos resolvers dos campos. Assim provavelmente vai ser complicado testar esse código e por consequência evoluí-lo. Se você quer saber mais como podemos fazer essa separação acompanha os próximos posts e compartilha essa série de tutoriais com as pessoas que você acha que gostariam de aprender mais sobre GraphQL!

GraphQL + Rails na prática – Grafo

Nos outros posts vimos o básico para começar a expor dados através de tipos, mas o real poder de GraphQL vem quando organizamos os dados em um grafo.

A ideia básica é que criemos nós (os tipos) e relacionamentos entre eles, ao invés de focar em apenas expô-los. Dessa forma os consumidores da API podem navegar pelos relacionamentos entre nós e extrair exatamente a informação que precisam.

Adicionando relacionamentos

Até agora temos dois tipos definidos (nós desse grafo): GameType e CharacterType. Para adicionar o relacionamento entre eles, basta criarmos um novo campo que vai retornar um desses tipos. Vamos começar adicionando as personagens aos jogos:

Types::GameType = GraphQL::ObjectType.define do
  name 'Game'
  description 'Type for Game models'
field :name, !types.String
  field :description, types.String
  field :launch_year, !types.String
  field :characters, types[Types::CharacterType]
end

O campo ‘:characters’ é do tipo ‘types[Types::CharacterType]’ (que é a maneira de representar uma lista de ‘CharacterType’) e como o nome do campo é o mesmo nome do atributo no nosso modelo ‘Game’, será retornado a lista de personagens do jogo.

Fazendo algo semelhante no tipo ‘CharacterType’ podemos construir a relação muitos-para-muitos que temos nos nossos modelos.

Types::CharacterType = GraphQL::ObjectType.define do
  name 'Character'
description 'Type for Character models'

field :name, !types.String
field :games, types[Types::GameType]
end

Navegando pelo grafo

Agora podemos modificar a consulta para buscar a lista de jogos incluindo também os personagens, basta fazer:

query {
game(id: 1) {
name
characters {
name
}
}
}

E se precisarmos da lista de jogos em que cada uma dessas personagens apareceu, basta navegar do personagem para o jogo:

query {
  game(id: 1) {
    name
    characters {
      name
    }
  }
}

Consultas flexíveis

Imagine agora como seria essa mesma implementação utilizando o padrão REST. Uma vez que fizemos o request para pegar informações de um jogo, como poderíamos pegar as personagens daquele jogo?

Uma alternativa seria ao buscar um jogo retornar links para as personagens, o que várias APIs fazem (como a do GitHub). Assim, dado a resposta inicial, podemos fazer outras requisições para obter as personagens. Mas isso exigiria várias outras chamadas, comparando com o GraphQL onde apenas uma chamada HTTP é feita.

Outra opção seria retornar as informações das personagens junto com o jogo. O problema com essa abordagem é que talvez nem todos os consumidores precisem dessa informação, além do que acoplamos fortemente os dois recursos. Utilizando GraphQL ambos os recursos são definidos em seus próprios tipos, com baixo acoplamento, e fica a cargo de quem consome decidir quais informações precisa.

No próximo post da série vamos mostrar como podemos criar e alterar dados utilizando Mutations. Se você está curtindo essa série sobre GraphQL avisa lá no twitter e acompanhe os próximos passos!

GraphQL + Rails na prática – Models e Types

No post anterior vimos como configurar uma aplicação Rails API para expor um endpoint GraphQL. Agora vamos entender como podemos expor objetos modelos de maneira fácil via GraphQL.

Criando modelos

Como vamos expor informações sobre jogos e seus personagens, vamos começar criando um modelo Game que vai conter o nome, descrição e o ano que o jogo surgiu.

bundle exec rails generate model Game name:string description:text launch_year:string

O próximo passo é criar um modelo para representar os personagens, vamos guardar apenas o nome do personagem por enquanto.

bundle exec rails generate model Character name:string

Agora precisamos criar uma relação entre Game e Character, já que um jogo possui muitas personagens e uma personagem pode aparecer em vários jogos. Vamos criar uma tabela de associação chamada para mapear os ids dos jogos e os ids das personagens.

bundle exec rails generate model Project game:references character:references

Agora basta adicionar as relações entre games e characters através dos projetos:

# app/models/game.rb
class Game < ApplicationRecord
 has_many :projects
 has_many :characters, through: :projects
end

# app/models/character.rb

class Character < ApplicationRecord
 has_many :projects
 has_many :games, through: :projects
end

O último passo é popular os modelos com alguns dados de exemplo utilizando o arquivo 'db/seeds.rb'. Coloque o seguinte código no arquivos e devemos estar prontos pra começar:

# db/seeds.rb
# personagens
mario = Character.create(name: "Mario")
peach = Character.create(name: "Peach")
dk = Character.create(name: "Donkey Kong")
zelda = Character.create(name: "Zelda")

# games
mario_world = Game.create(name: "Super Mario World", description: "SNES game", launch_year: "1990", characters: [mario, peach])
mario_kart = Game.create(name: "Mario Kart 8", description: "Wii U Game", launch_year: "2014", characters: [mario, peach, dk])
smash_bros = Game.create(name: "Super Smash Bros Wii U", description: "Wii U Game", launch_year: "2014", characters: [mario, peach, dk, zelda])
# adicionando games aos personagens
mario.games << [mario_world, mario_kart, smash_bros]
mario.save

peach.games << [mario_world, mario_kart, smash_bros]
peach.save

zelda.games << [smash_bros]
zelda.save

dk.games << [mario_kart, smash_bros]
dk.save

Agora é só rodar as migrações e criar os dados de exemplo

bundle exec rails db:create db:migrate db:seed

Criando tipos GraphQL

Crie um arquivo novo dentro de ‘app/graphql/types/’ chamado ‘game_type’ e nele vamos criar o novo tipo (bem semelhante ao que já está definido em ‘query_type.rb’). Vamos criar uma constante ‘Types::GameType’ que ira guardar o novo tipo:

# app/graphql/types/game_type.rb
Types::GameType = GraphQL::ObjectType.define do
end

Agora vamos criar os campos que serão expostos via GraphQL e definir seus tipos. Como vamos expor exatamente os mesmos atributos dos modelos (com os mesmos nomes), não é necessário criar um ‘resolve’ para cada campo, basta definir o campo e seu tipo:

# app/graphql/types/game_type.rb
Types::GameType = GraphQL::ObjectType.define do
  field :name, !types.String
  field :description, types.String
  field :launch_year, !types.String
end

Criando um consulta

Agora, para conseguirmos expor os dados de um jogo precisamos criar um query que retorne esse query type. Vamos na query raiz em ‘app/graphql/types/query_type.rb’ e lá criaremos uma consulta onde, dado um id, será retornado um GameType.

# app/graphql/types/query_type.rb
Types::QueryType = GraphQL::ObjectType.define do
  field :game do
    type Types::GameType
    argument :id, !types.Int
    resolve -> (root, args, ctx) {
      Game.find(args[:id])
    }
  end
end

Essa definição de query tem algumas diferenças, o ‘type’ é aquela constante que criamos ‘Types::GameType’, também existe um ‘argument’ chamado ‘:id’ e do tipo ‘types.Int’ (a exclamação indica que é um argumento obrigatório). No ‘resolve’ passamos um bloco com alguns argumentos (não importantes por hora) e fazemos a busca pelo id do ‘Game’.

Agora se abrirmos o GraphQL App de novo apontando para http://localhost:3000/graphql poderemos executar a seguinte consulta:

query {
  game(id: 1) {
    name
    description
    launch_year
  }
}

Screen Shot 2018-02-18 at 11.32.57

O legal das consultas com GraphQL é que elas dão uma flexibilidade bem legal para quem consome a API, podemos escolher exatamente quais dados queremos receber e teremos apenas eles na resposta.

Seguindo uma ideia parecida podemos criar um tipo para representar uma personagem e uma query para expô-la. Criar uma consulta para cada tipo funciona de maneira similar ao REST mas para entender o que faz GraphQL ser tão legal precisamos pensar em grafos! Se você curtiu conhecer mais sobre GraphQL avisa lá no twitter e fica ligado nos próximos posts!