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!