Tutorial: GraphQL com Exemplos. (#GraphQL #Python #NodeJs #dev #tutorial)

Neste post vou explicar o que é GraphQL, como ele funciona e fazer uma demonstração das principais funcionalidades dele. A grosso modo, ele é um padrão de API que fornece uma alternativa mais eficiente e flexível que o REST. Foi desenvolvido pelo Facebook em 2012 e é uma ferramenta open source (código fonte aqui).

Resumidamente, GraphQL permite buscar dados em uma API de maneira simples, utilizando apenas um endpoint e devolvendo somente os dados requisitados.

Introdução

Em grande parte das aplicações atuais, sempre que ela precisa de uma informação que está armazenada no banco de dados, uma API serve como intermediário para buscar estes dados e devolve-los de forma padronizada e segura. É bastante comum encontrarmos APIs criadas utilizando padrão REST e neste post vou falar sobre outro padrão: O GraphQL.

Frequentemente confundido com uma tecnologia de banco de dados, ele é uma linguagem de consulta para APIs. Mas antes de começar a explicar melhor sobre o GraphQL em si, uma breve revisão sobre REST (se quiser saber sobre rest, fiz este post aqui).

 

Qual o problema com o REST?

REST é uma maneira popular de implementar uma API. Quando o conceito de REST foi desenvolvido (lá por volta do ano 2000), o mundo era outro, a velocidade de desenvolvimento era outra e os requisitos dos softwares também. No entanto, o cenário da API mudou radicalmente nos últimos dois anos. Em particular, existem três fatores que desafiam a maneira como as APIs são projetadas:

  • APIs REST respondendo requisições apenas com payloads gigantes enquanto ocorre o aumento do uso de dispositivos móveis, de baixa potência e que utilizam redes de dados que não são (nem de longe) tão boas quanto as operadoras dizem nas propagandas.
  • Variedade de estruturas e plataformas de frontend, fazendo com que a API precise se adaptar para atender todos os requisitos. Com o tempo, a API fica com endpoints demais. (Viva os BFFs!)
  • Deploy contínuo é um padrão cada vez mais adotado nas empresas, com as APIs REST, a maneira como os dados são expostos pelo servidor geralmente precisa ser modificada para atender a requisitos específicos e alterações de design no lado do cliente. Isso dificulta práticas de desenvolvimento rápido e iterações de produtos.

 

Razões para utilizar GraphQL

  • Utiliza tipagem forte e tem um schema bem definido, o que diminui muito a chance de erro.
  • Resolve o problema do overfetching, que é quando, devido a estrutura da API, você acaba buscando mais dados do que realmente precisa para trabalhar.
  • Resolve o problema do underfetching, que é o problema oposto do anterior: quando o endpoint da API não retorna dados suficientes e isso significa que serão necessárias várias requisições.
  • Permite desenvolvimento rápido, pois possui várias bibliotecas que aceleram o desenvolvimento da API e a lógica de busca dos dados também fica simplificada (ficará mais claro no exemplo).
  • É possível combinar diversos schemas em uma única API através da utilização de stitching.

 

Componentes principais do GraphQL

  • Schema: Parte central do processo. Onde está descrita toda a estrutura da API.
  • Type: Utilizado para compor os schemas com tipos que não são só os primitivos.
  • Input: Mesmo esquema do Type, mas utilizado como argumento nas mutações.
  • Query: É a forma que se utiliza para buscar dados. Ela é composta por campos (fields) e argumentos (arguments). (Neste post, as vezes chamo Query de rota, mas vale lembrar que que no servidor existe apenas 1 rota.)
  • Mutation: Forma de alterar/manipular dados.
  • Resolver: Estes são os métodos utilizados para fazer o parse das informações. Ele “traduz” os tipos do schema para objetos.

 

Convenções de nomenclatura do GraphQL

  • Propriedades e campos: Utilizar camelCase;
  • Nomes dos tipos (type) e dos enums: Estes são escritos com PascalCase;
  • Valores dos Enums: TUDO_EM_CAPS;
  • Objetos input: Deve ser apenas um objeto com nome único e relacionado ao local onde ele vai ser utilizado e deve ser obrigatório (! no final);
  • Propriedades contendo listas devem ter seu nome no plural;
  • Ao definir nomes das mutações, coloque primeiro o nome do alvo da mutação e depois a ação:
    • Exemplo: reviewCreate e reviewDelete ao invés do que normalmente faríamos que é createReview e deleteReview.
    • Isso foi convencionado, pois o GraphQL não tem uma forma nativa de agrupar ou organizar os métodos, então utiliza-se a ordem alfabética para isso. No início isso parece um pouco estranho, mas depois parece que está no início.

 

Implementações de demonstração

Preparei dois exemplos de servidores GraphQL, o primeiro implementado em Python e em NodeJs e outro (mais simples) implementado apenas em Python.

Inicialmente, a minha ideia com este post era ter o mesmo servidor GraphQL implementado em 3 linguagens: Python, NodeJs e C#. O problema é que percebi que o post, além de ficar muito maior do que eu planejada, ia ficar confuso, pois teria que explicar a mesma coisa em 3x (uma para cada linguagem) ou  criar 3 versões do mesmo post.

Por uma questão de tempo e praticidade, mudei um pouco o escopo. Existe no repositório um servidor implementado em Python e NodeJs (também por uma questão de tempo, desisti de fazer tudo em C#). Neste exemplo, os dados são persistidos através da utilização de um banco SQLite e tem sistema para criação de usuários e autenticação utilizando JWT.

Para demonstrar aqui no post, criei uma versão bem mais simplificada, mas que vai exemplificar tudo que foi aplicado no outro servidor. Então se você acompanhar o exemplo por aqui, depois consegue aproveitar este conhecimento na outra implementação, que é mais completa.

 

O que é o exemplo feito em Python e NodeJs?

Ele é uma API GraphQL para um site no estilo do Twitter, mas lá em 2006 (em termos de funcionalidade). Nele você pode criar um usuário, fazer login, criar posts, listas posts e usuários, etc.

Ele foi feito com a abordagem schema-first e tem essa definição:

type Query {
    posts(skip: Int, take: Int): PostsPayload
    me: UserPayload
    users(skip: Int, take: Int): UsersPayload
}

type Mutation {
    postCreate(input: PostInput!): PostPayload
    postDelete(postId: Int!): PostPayload
    signUp(input: SignUpInput!, bio: String!): AuthPayload
    signIn(username: String!, password: String!): AuthPayload
}

type Post {
    id: ID!
    title: String!
    content: String!
    createdAt: String!
    author: User!
}

type User {
    id: ID!
    name: String!
    email: String!
    username: String!
    profile: Profile
    posts: [Post!]!
}

type Profile {
    id: ID!
    bio: String!
    isOwnProfile: Boolean!
    user: User!
}

type Error {
    message: String!
}

type PostsPayload {
    errors: [Error!]!
    posts: [Post!]!
}

type UserPayload {
    errors: [Error!]!
    user: User
}

type UsersPayload {
    errors: [Error!]!
    users: [User!]!
}

type PostPayload {
    errors: [Error!]!
    post: Post
}

type AuthPayload {
    errors: [Error!]!
    token: String
}

input PostInput {
    title: String!
    content: String!
}

input SignUpInput {
    name: String!
    username: String!
    email: String!
    password: String!
}

Link:

 


 

Exemplo prático

Deste ponto em diante, para seguir o tutorial você vai precisar de ter Python instalado.

Antes de começar:

  • O que será construído é uma API onde você pode listar produtos e incluir ou excluir reviews para cada um deles;
  • Utilizarei aqui a abordagem schema-first ao invés de code-first;
  • Caso precise conferir, o código completo deste exemplo está aqui: https://github.com/brenordv/demo-graphql-server/tree/master/simple_server_python
  • O pacote ariadne foi escolhido, por ser de implementação fácil e com footprint pequeno.

Instalando dependências

Para instalar as dependências, rode o comando abaixo:

pip install Flask==2.1.1 flask-cors==3.0.10 ariadne==0.15.1 simple-log-factory==0.0.1

Se preferir pegar o arquivo requirements.txt do repositório, pode instalar assim:

pip install -r requirements.txt

Preparando a IDE

Tecnicamente, você não precisa instalar nada além dos pacotes para seguir o tutorial, mas para melhorar a integração com a sua IDE, procure pelos plugins do GraphQL. Eles vão ajudar e estão disponíveis para VS Code e as IDEs da JetBrains.

 

Criando a primeira “rota” no servidor

Na pasta raiz do código, crie um arquivo chamado schema.graphql (não tem obrigação de ser neste local ou ter este nome, mas é uma convenção útil para manter organizado).

Este arquivo possuirá as definições de tudo que o nosso servidor será capaz de fazer e o primeiro endpoint será um simples “Hello World”. Sendo assim, coloque no arquivo:

type Query {
    hello: String!
}

Com esse código, definimos uma das formas de se buscar dados (Query) e esta forma foi através de uma requisição para “hello”. Esta rota é obrigada a retornar uma string. Ela nunca vai retornar nulo (pode até ser uma string vazia, mas nunca nulo).  Como eu sei disso? Por causa da exclamação no final do tipo da resposta. Se essa rota fosse sem isso (hello: String), então eu poderia retornar uma string ou nulo. Como temos uma exclamação (hello: String!), só posso retornar string.

Esta é uma das partes legais do GraphQL. Os contratos são muito específicos e isso facilita a utilização e estabilidade dos sistemas.

 

Preparando o servidor GraphQL

Primeiro vamos criar o servidor mais simples possível, apenas para ver se está tudo funcionando corretamente.

Para organizar o código, crie uma pasta/pacote na raiz do projeto e a chame de src. Sempre que falar que é para criar um arquivo na raiz do projeto, estou me referindo a esta pasta.

Na pasta raiz, crie um arquivo chamado settings.py e inclua o seguinte código nele:

# -*- coding: utf-8 -*-
import logging

from simple_log_factory.log_factory import log_factory

LOGGER: logging = log_factory(log_name="GRAPHQL-SERVER", log_time_format="%H:%M:%S")
SERVER_VERSION = "1.0.0"
REQUEST_COUNT: int = 0


def inc_request_count():
    global REQUEST_COUNT
    REQUEST_COUNT += 1


def get_request_count():
    global REQUEST_COUNT
    return REQUEST_COUNT

O que este código faz? Não muito. Estou apenas deixando o logger preparado para ser utilizado em outros lugares e deixando algumas funcionalidades que farão sentido mais para frente no tutorial.

 

Ainda na pasta raiz do código, crie o arquivo app.py e inclua o seguinte código nele:

# -*- coding: utf-8 -*-
from ariadne import ObjectType, snake_case_fallback_resolvers, make_executable_schema, load_schema_from_path, \
    graphql_sync
from ariadne.constants import PLAYGROUND_HTML
from flask import Flask, url_for, redirect, request, jsonify
from flask_cors import CORS

from src.settings import inc_request_count, get_request_count

app = Flask(__name__)
CORS(app)

Neste código, estou criando a aplicação Flask e deixando CORS configurado de forma padrão. Aproveitei para deixar também os imports que vamos precisar.

Agora vamos criar, em código, a definição de como o servidor deve tratar cada requisição:

query = ObjectType("Query")
query.set_field("hello", lambda x, y: "Hello World!")

Para cada “tipo” criado no schema, precisamos ter um equivalente em código. O que faz sentido, pois se no schema declaramos que é possível utilizar Query, temos que programar também como estas queries serão interpretadas (calma, é mais simples do que está parecendo).

No código acima:

  • Na primeira linha eu inicializei um objeto para definir tipo e falei que ele vai ter as definições das queries.
  • Na segunda linha, no método set_field, defini que a rota “hello” será respondida pelo resultado da função lambda que está la. (Vamos passar melhor por estas funções e vou explicar melhor o que é cada um dos argumentos).

 

Agora vamos inicializar o schema:

type_defs = load_schema_from_path("schema.graphql")
schema = make_executable_schema(
    type_defs, query, snake_case_fallback_resolvers
)

No código acima, primeiro eu carrego o arquivo schema.graphql criado anteriormente. Neste exemplo, eles estão na mesma pasta, mas se no exemplo que você está fazendo ele estiver em outro lugar, então informe o caminho para chegar no arquivo.

Logo depois criei a instância do schema, passando para ele a definição dos tipos (type_defs, que contém o que foi lido do arquivo), as definições que fizemos de como responder as queries e um resolver muito interessante, que é o snake_case_fallback_resolvers. Ele serve para traduzir os nomes escritos no padrão camelCase para o snake_case. Desta forma, você não precisa escrever funções, nomes de argumento, etc. fugindo dos padrões de nomenclatura do Python.

Você pode estar se perguntando “O que é um resolver?” Calma, vou explicar ele mais para frente. No momento, o próximo passo é criar os métodos base na API Flask:

def _get_context(req):
    inc_request_count()
    return {
        "request": req,
        "request_count": get_request_count()
    }


@app.route("/graphql", methods=["GET"])
def graphql_playground():
    return PLAYGROUND_HTML, 200


@app.route("/", methods=["GET", ])
def index():
    return redirect(url_for("graphql_playground")), 302


@app.route("/graphql", methods=["POST"])
def graphql_server():
    data = request.get_json()
    success, result = graphql_sync(
        schema,
        data,
        context_value=_get_context(request),
        debug=app.debug
    )
    status_code = 200 if success else 400
    return jsonify(result), status_code

Criamos 4 métodos:

  1. def _get_context(req): Este é um método helper, que vai ser utilizado para demonstrar como passar informações das request através do contexto. Ele vai ser chamado todas as vezes que uma requisição for recebida. É importante entender que este método vai retornar um objeto com as informações que estarão disponíveis como contexto em todos os resolvers e é executada uma vez por requisição.
  2. def graphql_playground(): Rota responsável por devolver o playground que vem com o pacote ariadne. Este playground, a grosso modo, é algo tipo o Swagger e vai te permitir testar as rotas sem precisar de um postman ou algo do tipo.
  3. def index(): Criei esta rota de conveniência. Desta forma, assim que acessar localhost, você será redirecionado para o playground. Sem esta rota, você teria que acessar localhost:5000/playground.
  4. def graphql_server(): Esta é a rota que vai ser mais utilizada do servidor. Todas as requisições do servidor vão para este endpoint.
    Note que neste método, na execução do graphql_sync, no argumento context_value, estou passando o resultado do método _get_context, ou seja, tudo que ele retornar estará disponível para utilização dentro dos resolvers.

 

A última coisa que falta para fazermos o primeiro teste é colocar o código para fazer o servidor Flask ser executado:

if __name__ == '__main__':
    app.run("0.0.0.0", 5000)

 

Agora execute o código e acesse localhost:5000. Você será redirecionado para o playground. No campo da esquerda (que é o utilizado para escrever as requisições), utilize a seguinte query:

query {
  hello
}

 

Agora clique no botão Play e você deve receber o resultado abaixo. Parabéns! Você fez a primeira query com GraphQL. 😀

{
  "data": {
    "hello": "Hello World!"
  }
}

Esta rota foi simples, não precisou de nenhum parâmetro e recebemos uma string como retorno. Agora vamos incluir uma rota levemente mais elaborada.
Edite o arquivo schema.graphql e substitua o código dele pela definição abaixo:

type Query {
    hello: String!
    myName(myNameIs: String!): String!
}

Com esta alteração, nosso servidor agora tem 2 queries disponíveis. A nova query (myName) recebe uma string como argumento (que não pode ser nulo, como indicado pela exclamação) e também recebe como resposta uma string.

Da mesma forma que fizemos com a query “hello”, temos também que definir como o servidor irá responder quando receber uma request para a query myName. Sendo assim, logo abaixo da definição da query “hello”, inclua o código abaixo:

query.set_field("myName", lambda x, y, myNameIs: f"Hi {myNameIs}! Nice to meet you!")

Repare que no código acima, a função lambda recebe os mesmos argumentos x e y (daqui a pouco vou explicar o que eles são) e também recebe um terceiro, que é o que foi definido la no schema.
Agora execute a api novamente, abra o playground e utilize esta query:

query {
  myName(myNameIs:"Raccoon")
}

O resultado deve ser este:

{
  "data": {
    "myName": "Hi Raccoon! Nice to meet you!"
  }
}

Agora nosso servidor já sabe responder a duas queries, mas o código está ficando confuso. Estas lambdas que estão respondendo as requisições não estão ajudando muito o código a ficar limpo. Elas estão fazendo o papel de resolvers. Os resolvers são os métodos que vão processar as requisições para aquela query ou mutation e vão devolver o resultado. A grosso modo seriam como se fossem os métodos de uma controller.

Os resolvers nada mais são que métodos simples. Por padrão, eles recebem 2 argumentos (que estava chamando de x e y antes): O primeiro é o objeto pai (vai fazer sentido em breve) e o info, que possui informações sobre a requisição. É desse argumento que você consegue acessar as informações de contexto. Todos os argumentos definidos no schema são informados na sequência. Por isso a nossa query “myName” possui 3 argumentos (os 2 padrões e mais o exigido pela query) enquanto a “hello” tem só os padrões.

 

Criando e organizando resolvers

Agora que o servidor GraphQL que estamos fazendo está funcionando, antes de continuarmos com conceitos um pouco mais elaborados, precisamos organizar um pouco as coisas.

Na pasta raiz do código, crie um pacote chamado resolvers e nele crie um arquivo chamado query_resolvers.py e inclua o seguinte código:

# -*- coding: utf-8 -*-
from src.settings import LOGGER


def query_hello_resolver(obj, info) -> str:
    LOGGER.info(f"Resolving: Query.hello | Query number={info.context.get('request_count')}")
    return "Hello, World!"

O código acima é o resolver mais básico que tem. Ele recebe os dois argumentos padrões, não recebe mais nada e retorna uma string (conforme definição no schema do servidor).
Para fins didáticos, vou incluir em todos os resolvers um log, para que fique fácil de visualizar quando cada um é chamado. Note que neste log eu utilizo a propriedade context do argumento info e extraio o número de requisições que o servidor já recebeu (request_count).

De onde veio esta informação? Do método _get_context(req) que definimos lá no início e o número de requisições fica definido em uma variável lá no arquivo settings.py. No outro exemplo que existe no repositório, utilizo esta mesma estratégia para passar informações extraídas do token JWT do usuário.

Agora vamos aplicar este novo resolver! No arquivo app.py, substitua a linha:

query.set_field("hello", lambda x, y: "Hello World!")

 

Por:

from src.resolvers.query_resolvers import query_hello_resolver
query.set_field("hello", query_hello_resolver)

Se você executar o servidor, vai ver que o resultado da query vai ser o mesmo, mas no terminal onde o Flask está sendo executado vai mostrar também uma linha de log com a contagem de quantas requisições o servidor recebeu.

 

Agora vamos incluir o resolver para a segunda query que fizemos. Para isso, volte no arquivo query_resolvers.py e inclua o código abaixo:

from ariadne import convert_kwargs_to_snake_case

@convert_kwargs_to_snake_case
def query_myname_resolver(obj, info, my_name_is: str) -> str:
    LOGGER.info(f"Resolving: Query.myName | Query number={info.context.get('request_count')}")
    return f"Hi {my_name_is}! Nice to meet you!"

Este resolver é bem parecido: Fazemos o log e respondemos com uma string. Existem duas diferenças nele: Temos o terceiro argumento, com o nome escrito utilizando snake_case e um decorator (@convert_kwargs_to_snake_case) logo antes da chamada deste resolver.

O que acontece é que, apesar do nome da variável estar com camelCase no schema do servidor (myNameIs), quando utilizamos este decorator, ele converte o nome de todas as variáveis que estão chegando para o padrão snake_case.

Como o argumento é obrigatório (myNameIs: String!), então defini o tipo da variável como str. Se fosse opcional (sem o !), poderia colocar Union[str, None], pois poderia vir nulo.

Nota importante: Os argumentos que chegarão serão sempre strings ou objetos. O tipo da variável não influencia isso, ou seja, se você espera receber um int, vai ter que converter de str para int.

 

Agora vamos aplicar este novo resolver. Vai um procedimento parecido com o anterior. Volte no arquivo app.py e substitua a linha:

query.set_field("myName", lambda x, y, myNameIs: f"Hi {myNameIs}! Nice to meet you!")

 

Por:

from src.resolvers.query_resolvers import query_myname_resolver
query.set_field("myName", query_myname_resolver)

Talvez seja importante apontar que estou apenas passando a referência para o método do resolver, mas ele não deve ser executado neste momento.

Até aqui tudo bem, mas e se quisermos retornar algo mais complexo que um dado de tipo primitivo (string, int, float, boolean, etc)?

Vamos fazer uma query que retorna um objeto.

 

Criando query que retorna um objeto

Para exemplificar esta funcionalidade, vamos criar uma query que retorne o número de requisições que o servidor recebeu e a versão dele.

Abra o arquivo schema.graphql e logo depois da definição de Query, insira o código abaixo:

type ReqCountPayload {
    count: Int!
    serverVersion: String!
}

No código acima definimos um novo tipo de dado. É como se tivéssemos declarado uma classe, ou seja, agora podemos utilizar este tipo como referência dentro do esquema. Apenas para recapitular: Este objeto possui duas propriedades, ambas obrigatórias.

 

Substitua a definição da query pelo código abaixo:

type Query {
    hello: String!
    myName(myNameIs: String!): String!
    reqCount: ReqCountPayload!
}

Neste caso, estamos falando que a query reqCount sempre vai retornar um objeto com a estrutura definida no ReqCountPayload e ele, por sua vez, sempre vai retornar um inteiro na propriedade count e uma string na propriedade serverVersion.

Acesse o arquivo query_resolvers.py e vamos criar o resolver para esta nova query.

from src.settings import get_request_count, SERVER_VERSION
def query_req_count_resolver(obj, info) -> dict:
    LOGGER.info(f"Resolving: Query.reqCount | Query number={info.context.get('request_count')}")
    return {
        "count": get_request_count(),
        "server_version": SERVER_VERSION
    }

Aqui temos alguns pontos importantes. Repare que não temos o decorator neste resolver e estamos retornando um objeto onde uma das propriedades está escrita com o padrão snake_case.

Isso vai funcionar, pois lá no arquivo app.py, quando chamamos o método make_executable_schema, incluímos também o snake_case_fallback_resolvers. Esse objeto converte os nomes de propriedades de volta para camelCase.

A diferença entre ele e o decorator (@convert_kwargs_to_snake_case) é que o decorator funciona nos argumentos que estão chegando no método do resolver, enquanto que o snake_case_fallback_resolvers funciona no retorno dos métodos.

 

Vamos agora incluir o mapeamento para a query reqCount lá no app.py. Para isso, inclua o código:

from src.resolvers.query_resolvers import query_req_count_resolver
query.set_field("reqCount", query_req_count_resolver)

 

Executando esta nova query

Até separei a execução desta nova query em um tópico diferente, pois aqui vou explicar um conceito chave do GraphQL.

Se você for no playground e executar esta query:

query {
  reqCount
}

 

O seu resultado vai ser:

{
  "error": {
    "errors": [
      {
        "locations": [
          {
            "column": 3,
            "line": 2
          }
        ],
        "message": "Field 'reqCount' of type 'ReqCountPayload!' must have a selection of subfields. Did you mean 'reqCount { ... }'?"
      }
    ]
  }
}

O que aconteceu? O que está errado?

Um dos pontos principais do GraphQL é que, quando você pede uma informação (faz uma request), você também tem que falar o que vai utilizar do resultado. Se este fosse uma API REST e nós quiséssemos apenas a versão do servidor, teríamos que fazer uma request, pegar o objeto inteiro no retorno e utilizar apenas a informação de versão. Com GraphQL isso não acontece. Quando você fizer a request, já vai falar o que precisa e vai receber apenas o que tiver pedido.

Sendo assim, a query falhou não por causa de algum erro de programação, mas por não termos especificado o que vamos querer do retorno.

Se eu quiser apenas o número de requisições que o servidor já recebeu, minha query ficaria assim:

query {
  reqCount {
    count
  }
}

 

Resultado:

{
  "data": {
    "reqCount": {
      "count": 1
    }
  }
}

 

Se eu quiser apenas a versão do servidor, minha query ficaria assim:

query {
  reqCount {
    serverVersion
  }
}

 

Resultado:

{
  "data": {
    "reqCount": {
      "serverVersion": "1.0.0"
    }
  }
}

 

Nada me impede de pedir o objeto inteiro. Neste caso, a query ficaria assim:

query {
  reqCount {
    count
    serverVersion
  }
}

 

Resultado:

{
  "data": {
    "reqCount": {
      "count": 3,
      "serverVersion": "1.0.0"
    }
  }
}

Nosso próximo passo neste post (que está virando um livro sobre GraphQL) é criar uma query para retornarmos os produtos que estão cadastrados. Lembre-se: a ideia final deste tutorial é ter um servidor GraphQL onde você pode fazer reviews de produtos.

 

Criando query para retornar os produtos cadastrados

No outro exemplo, estou utilizando SQLite, mas para manter este tutorial simples, vou deixar os produtos hard-coded. Sendo assim, para definirmos os produtos disponíveis, crie na pasta raiz um arquivo chamado data.py e nele insira esta lista:

# -*- coding: utf-8 -*-

PRODUCTS = [
    {
        "id": 1,
        "name": "USB Type-C cable - 3m",
        "price": 14.22,
    },
    {
        "id": 2,
        "name": "Red Tomato",
        "price": 4.14,
    },
    {
        "id": 3,
        "name": "Generic Beer",
        "price": 2.99,
    },
    {
        "id": 4,
        "name": "Cell phone charger",
        "price": 30.00,
    },
    {
        "id": 5,
        "name": "Replica of Obiwan Kenobi Lightsaber",
        "price": 99.50,
    },
    {
        "id": 6,
        "name": "Mouse Gamer",
        "price": 78.55,
    },
    {
        "id": 7,
        "name": "Gamer girl bath water",
        "price": 200.12,
    },
]

Nesta lista temos 7 produtos aleatórios, cada um com: id, nome e valor.

Vamos Incluir de uma vez algumas reviews:

from datetime import datetime
REVIEWS = [
    {
        "id": 1,
        "product_id": 5,
        "author": "starWarsFan321",
        "content": "Great product! Excellent build quality!",
        "grade": 4,
        "date_added": datetime.utcnow()
    },
    {
        "id": 2,
        "product_id": 5,
        "author": "darthSider11",
        "content": "This replica of Obiwan Kenobi's lightsaber is terrible! The blade is flimsy and the hilt feels cheap. I would not recommend this product to anyone.",
        "grade": 1,
        "date_added": datetime.utcnow()
    },
    {
        "id": 3,
        "product_id": 5,
        "author": "warOfTheStars99",
        "content": "This is the best replica of an Obiwan Kenobi lightsaber that I have ever seen! It is so realistic and looks just like the real thing!",
        "grade": 5,
        "date_added": datetime.utcnow()
    },
    {
        "id": 4,
        "product_id": 3,
        "author": "beerGourmetPerson",
        "content": "This beer is pretty generic. It's not great, but it's not terrible. It's just kind of... there. If you're looking for a beer that won't offend anyone and will just get the job done, this is the one for you. But if you're looking for!",
        "grade": 3,
        "date_added": datetime.utcnow()
    },
    {
        "id": 5,
        "product_id": 3,
        "author": "drunk33",
        "content": "If you're looking for a cheap, generic beer, then this is the one for you! It's watery and tasteless, but it'll get the job done.",
        "grade": 4,
        "date_added": datetime.utcnow()
    },
    {
        "id": 6,
        "product_id": 7,
        "author": "simpLePerson123",
        "content": "I don't know what all the fuss is about. It's just water.",
        "grade": 3,
        "date_added": datetime.utcnow()
    },
    {
        "id": 7,
        "product_id": 7,
        "author": "superFan",
        "content": "smells amazing! <3",
        "grade": 5,
        "date_added": datetime.utcnow()
    },
    {
        "id": 8,
        "product_id": 7,
        "author": "ultraFan",
        "content": "nice smell... tastes a little funky, though.",
        "grade": 5,
        "date_added": datetime.utcnow()
    },
    {
        "id": 8,
        "product_id": 7,
        "author": "GamerGirrrl",
        "content": "I'm a gamer girl and I completely misunderstood this product. DO. NOT. RECOMMEND.",
        "grade": 1,
        "date_added": datetime.utcnow()
    },
    {
        "id": 9,
        "product_id": 7,
        "author": "ai44",
        "content": "This gamergirl bath water is terrible! It's just a bunch of dirty water that smells like gamer sweat. Do not bother buying it.",
        "grade": 5,
        "date_added": datetime.utcnow()
    },
]

Agora temos reviews para quase todos os produtos.

Fun fact: Estava com preguiça de escrever as reviews, então utilizei o OpenAI e ele gerou o conteúdo das reviews para mim.

Cada uma das reviews tem: Id, Id do produto, autor, conteúdo da review, uma nota e a data em que foi adicionada.

 

Agora que temos estes objetos, vamos pensar na relação entre eles:

  • Um produto pode ter várias reviews, mas pode não ter sido avaliado ainda (ou seja: 0..n reviews).
  • Uma review vai pertencer a um Produto (ou seja: 1..1)

Ok. Com isso em mente, vamos agora declarar os tipos de Produto e Review no nosso schema.

Vamos fazer algumas alterações no arquivo schema.graphql…

Primeiro vamos substituir a definição de Query por esta:

type Query {
    hello: String!
    myName(myNameIs: String!): String!
    reqCount: ReqCountPayload!
    products: [Product!]!
}

O que a nova query quer dizer? Ela indica que vamos -obrigatoriamente- retornar uma lista e que esta lista pode até estar vazia, mas nunca terá um objeto nulo nela (pois tem um ! depois de Product).

 

Agora vamos definir o tipo de Product e Review:

type Product {
    id: ID!
    name: String!
    price: Float!
    reviews: [Review!]!
}

type Review {
    id: ID!
    author: String!
    content: String!
    grade: Int!
    product: Product!
    dateAdded: String!
}

Em Product, inclui uma propriedade chamada “reviews”, que é uma lista de Review. Esta lista pode vir vazia, mas nunca nula (e nela não existirão objetos nulos).

De maneira similar, na definição de Review eu inclui uma propriedade chamada “product”, que retornará o produto ao qual a review faz referência. Como uma review precisa, necessariamente, ter um produto vinculado, o retorno do produto é obrigatório (no sentido de que, se você quiser ver isso no resultado da query, ele não vai ser nulo).

Algumas observações importantes:

  • Na definição de produto, inclui uma nova propriedade, que não está no objeto que definimos la no arquivo data.py. Inclusive, não existem em lugar nenhum no produto, uma referência para as reviews dele, mas disponibilizamos isso aqui.
  • O objeto Review também não possui uma propriedade para o produto (objeto) ao qual ela se refere, mas tem uma propriedade com o id do produto (product_id) e isso é o suficiente.
  • O objeto Review tem uma propriedade chamada dateAdded. Em código ela é um datetime, mas ela é retornada como string (igual todas as outras APIs no mundo. Só queria apontar que não existe um tipo de dado específico para datetime).
  • Tanto Review quanto Product tem uma propriedade com tipo ID!. Isso é uma forma de indicar que esta propriedade é o identificador daquele tipo. É algo mais semântico, tecnicamente falando, não vai fazer muita diferença, continuaria funcionando se colocássemos Int! no lugar de ID!, mas é importante fazer esta distinção.

 

Neste ponto da vida, depois de seguir este tutorial, você já deve estar imaginando o próximo passo. Sim: criar resolvers.

 

Resolver para a query: products

Abra o arquivo query_resolvers.py e vamos incluir mais um resolver la:

from typing import List
from src.data import PRODUCTS
def query_products_resolver(obj, info) -> List[dict]:
    LOGGER.info(f"Resolving: Query.products | Query number={info.context.get('request_count')}")
    return PRODUCTS

No resolver acima estamos retornando a lista de produtos.

Agora vá até o app.py e inclua este mapeamento nas queries:

from src.resolvers.query_resolvers import query_products_resolver
query.set_field("products", query_products_resolver)

 

Neste momento você pode estar se perguntando: Ok, mas como que isso vai ser traduzido para o que definimos no schema? De onde vão surgir as reviews de cada produto?

 

Vamos agora aplicar um pouco da mágica do GraphQL. Dentro do pacote resolvers, crie um chamado type_resolvers e lá dentro crie o arquivo product_resolvers.py.

Neste novo arquivo, vamos incluir um resolver específico para a propriedade reviews do type Product (que definimos no schema e nenhuma relação com o objeto que está na nossa base):

# -*- coding: utf-8 -*-
from typing import List

from src.data import REVIEWS
from src.settings import LOGGER


def product_review_resolver(obj: dict, info) -> List[dict]:
    LOGGER.info(f"Resolving: Product.reviews | Query number={info.context.get('request_count')}")
    product_id = obj.get("id")
    reviews = [r for r in REVIEWS if r["product_id"] == product_id]
    LOGGER.info(f"Found {len(reviews)} reviews for product {obj.get('name')}")

    return reviews

O que está acontecendo nesse resolver?

Sabe este primeiro argumento (obj) que não utilizamos até agora? Então, ele tem os dados do objeto pai, ou seja, este resolver será chamado para objeto do tipo Product que o servidor estiver devolvendo. Sem que você tenha que fazer ações extras, todas as vezes que, no schema, estiver mapeado um Product, a propriedade reviews dele será resolvida por esta função e o argumento obj contém o produto em si.

Sendo assim, para pegar as reviews dele, basta pegar o id do produto e usar para filtrar as reviews pelo product_id.

 

Agora precisamos adicionar este resolver ao mapeamento que está feito la no app.py:

from src.resolvers.type_resolvers.product_resolver import product_review_resolver
product = ObjectType("Product")
product.set_field("reviews", product_review_resolver)

 

Agora precisamos adicionar este novo mapeamento aos argumentos que estão sendo passados para o método make_executable_schema. O código deve ficar assim:

schema = make_executable_schema(
    type_defs, query, product, snake_case_fallback_resolvers
)

Agora execute o servidor e faça a seguinte query:

query{
  products{
    name
    price
    reviews {
      grade
      content
      author
      dateAdded
    }
  }
}

A query acima vai buscar todos os produtos e receber de volta o nome e o preço de cada produto, a lista de reviews e para cada review vamos ter: a nota, o conteúdo, o autor e a data em que ele foi adicionado.

 

O resultado será assim:

{
  "data": {
    "products": [
      {
        "name": "USB Type-C cable - 3m",
        "price": 14.22,
        "reviews": []
      },
      {
        "name": "Red Tomato",
        "price": 4.14,
        "reviews": []
      },
      {
        "name": "Generic Beer",
        "price": 2.99,
        "reviews": [
          {
            "author": "beerGourmetPerson",
            "content": "This beer is pretty generic. It's not great, but it's not terrible. It's just kind of... there. If you're looking for a beer that won't offend anyone and will just get the job done, this is the one for you. But if you're looking for!",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 3
          },
          {
            "author": "drunk33",
            "content": "If you're looking for a cheap, generic beer, then this is the one for you! It's watery and tasteless, but it'll get the job done.",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 4
          }
        ]
      },
      {
        "name": "Cell phone charger",
        "price": 30,
        "reviews": []
      },
      {
        "name": "Replica of Obiwan Kenobi Lightsaber",
        "price": 99.5,
        "reviews": [
          {
            "author": "starWarsFan321",
            "content": "Great product! Excellent build quality!",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 4
          },
          {
            "author": "darthSider11",
            "content": "This replica of Obiwan Kenobi's lightsaber is terrible! The blade is flimsy and the hilt feels cheap. I would not recommend this product to anyone.",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 1
          },
          {
            "author": "warOfTheStars99",
            "content": "This is the best replica of an Obiwan Kenobi lightsaber that I have ever seen! It is so realistic and looks just like the real thing!",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 5
          }
        ]
      },
      {
        "name": "Mouse Gamer",
        "price": 78.55,
        "reviews": []
      },
      {
        "name": "Gamer girl bath water",
        "price": 200.12,
        "reviews": [
          {
            "author": "simpLePerson123",
            "content": "I don't know what all the fuss is about. It's just water.",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 3
          },
          {
            "author": "superFan",
            "content": "smells amazing! <3",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 5
          },
          {
            "author": "ultraFan",
            "content": "nice smell... tastes a little funky, though.",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 5
          },
          {
            "author": "GamerGirrrl",
            "content": "I'm a gamer girl and I completely misunderstood this product. DO. NOT. RECOMMEND.",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 1
          },
          {
            "author": "ai44",
            "content": "This gamergirl bath water is terrible! It's just a bunch of dirty water that smells like gamer sweat. Do not bother buying it.",
            "dateAdded": "2022-07-02 21:17:58.648545",
            "grade": 5
          }
        ]
      }
    ]
  }
}

 

O problema do N+1

Vale um momento para analisarmos o log do servidor após esta request:

 * Running on all addresses.
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Query.products | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Product.reviews | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Found 0 reviews for product USB Type-C cable - 3m
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Product.reviews | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Found 0 reviews for product Red Tomato
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Product.reviews | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Found 2 reviews for product Generic Beer
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Product.reviews | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Found 0 reviews for product Cell phone charger
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Product.reviews | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Found 3 reviews for product Replica of Obiwan Kenobi Lightsaber
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Product.reviews | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Found 0 reviews for product Mouse Gamer
18:18:00 - GRAPHQL-SERVER - INFO - Resolving: Product.reviews | Query number=1
18:18:00 - GRAPHQL-SERVER - INFO - Found 5 reviews for product Gamer girl bath water
192.168.15.42 - - [02/Jul/2022 18:18:00] "POST /graphql HTTP/1.1" 200 -

Veja que no log, a função do resolver de produtos foi chamada uma vez (Resolving: Query.products) e depois, sem que precisarmos tomar qualquer ação adicional, o resolver das reviews do produto foi chamado uma vez para cada produto na lista. Dependendo do volume de dados e do nível de ligação entre os tipos de dados, isso pode gerar um impacto grande na  hora de carregar os dados, mas existem formas de contornar isso com a utilização de dataloaders, mas não vou entrar neste ponto aqui, pois o tutorial está ficando muito grande (provavelmente devo fazer outro tutorial apenas para tratar este ponto).

 

Antes de passarmos para as últimas queries, temos que criar o resolver para o tipo Review.

 

Resolver para a Review

Este resolver vai servir para que as reviews que forem buscadas no sistema possam retornar com o seu produto.

No pacote type_resolvers, crie o arquivo review_resolver.py e inclua o código abaixo:

# -*- coding: utf-8 -*-
from ariadne import convert_kwargs_to_snake_case

from src.data import PRODUCTS
from src.settings import LOGGER


@convert_kwargs_to_snake_case
def review_product_resolver(obj: dict, info) -> dict:
    LOGGER.info(f"Resolving: Review.product | Query number={info.context.get('request_count')}")
    product_id = obj.get("product_id")

    # No validation, but it's OK. We know that product exists.
    product = [p for p in PRODUCTS if p.get("id") == product_id][0]
    LOGGER.info(f"Product '{product.get('name')}' found for this review.")

    return product

Para relembrar: O primeiro argumento que o resolver recebe é uma referência para o objeto pai, ou seja, ele consegue utilizar tudo que a Review tem. No caso, precisamos apenas do product_id. Então utilizamos ele para filtrar os produtos cadastrados. Neste caso, até seria interessante alguma validação, mas para manter o código simples, vamos partir da premissa de que todos os product_id estão apontando para um produto válido.

Agora precisamos mapear este resolver no app.py:

from src.resolvers.type_resolvers.review_resolver import review_product_resolver
review = ObjectType("Review")
review.set_field("product", review_product_resolver)

Ainda no arquivo app.py, inclua a variável review na chamada do método make_executable_schema:

schema = make_executable_schema(
    type_defs, query, product, review, snake_case_fallback_resolvers
)

 

Retornando uma review ou produto por Id

Considerando o que já fizemos neste tutorial, vamos incluir mais 2 queries: Uma para retornar um product (buscando por Id) e outro para retornar uma review por Id.

Para isso, abra o arquivo schema.graphql e substitua a definição de Query com o código abaixo:

type Query {
    hello: String!
    myName(myNameIs: String!): String!
    reqCount: ReqCountPayload!
    products: [Product!]!
    review(reviewId: ID!): Review
    product(productId: ID!): Product
}

Em ambas queries, vamos passar o id do objeto e vamos retornar um objeto (Review ou Product). Todavia, reparem que nestas queries o retorno não é obrigatório (não tem exclamação), ou seja, se você passar o id de um produto ou uma review que não existem, o resultado será nulo.

 

Agora abra o arquivo query_resolvers.py e inclua os dois métodos abaixo:

from typing import Union
from src.data import PRODUCTS, REVIEWS
@convert_kwargs_to_snake_case
def query_review_resolver(obj, info, review_id: int) -> Union[dict, None]:
    LOGGER.info(f"Resolving: Query.review | Query number={info.context.get('request_count')}")
    filtered_reviews = [r for r in REVIEWS if r.get('id') == review_id]
    if len(filtered_reviews) == 0:
        return None

    return filtered_reviews[0]


@convert_kwargs_to_snake_case
def query_product_resolver(obj, info, product_id: str) -> Union[dict, None]:
    LOGGER.info(f"Resolving: Query.product({product_id}) | Query number={info.context.get('request_count')}")
    filtered_product = [p for p in PRODUCTS if p.get('id') == int(product_id)]
    if len(filtered_product) == 0:
        return None

    return filtered_product[0]

A ação dos métodos são bem parecidas. No resolver de produtos, utilizo list comprehension para filtrar o produto com o id que foi informado. De forma similar, no resolver de Review, filtro a lista das reviews existentes.

Em ambos casos, se tiver encontrado um item com o id desejado, retorno este item. Se não, retorno nulo (estes resolvers podem ser melhorados, mas resolvi deixar eles mais simples aqui).

 

O próximo passo é mapear as queries com os resolvers que acabamos de criar (uau, que surpresa!). Sendo assim, abra o arquivo app.py:

from src.resolvers.query_resolvers import query_review_resolver, query_product_resolver
query.set_field("review", query_review_resolver)
query.set_field("product", query_product_resolver)

 

Agora que já está tudo mapeado, vamos testar estas queries novas antes de passarmos para as mutações.

 

No playground, utilize a query:

query {
  product(productId: 99) { 
    name
    price
    reviews {
      id
      content
      grade
      author
    }
  }
}

 

O resultado será:

{
  "data": {
    "product": null
  }
}

Veja que, como não possuímos um produto com id 99, o retorno foi nulo, mas apenas o retorno de produto e não a resposta completa do servidor, ou seja, o servidor não vai devolver apenas nulo. Ele vai devolver uma resposta padrão com o valor de produto nulo.

 

Se utilizarmos a mesma query, mas mudarmos o Id para 1 (ao invés de 99), o resultado será:

{
  "data": {
    "product": {
      "name": "USB Type-C cable - 3m",
      "price": 14.22,
      "reviews": []
    }
  }
}

Neste caso, o servidor retornou o produto com Id igual a 1, mas como ele não tem reviews, a propriedade retornou uma lista vazia. Isso devido ao mapeamento que fizemos no schema.

Só para recapitular:

  • [Review!]! significa: Lista de reviews, onde nenhum item da lista será nulo e a propriedade em si não será nula também (será retornada como lista vazia, caso não existam elementos).
  • [Review]! significa: Lista de reviews, onde algum item da lista poderá ser nulo, mas a propriedade em si não (será retornada como lista vazia, caso não existam elementos).
  • [Review] significa: Lista de reviews, onde algum item da lista poderá ser nulo e a propriedade em si também pode vir como nula.

 

Bom, agora que já fizemos várias rotas de consulta (Query), vamos fazer 2 rotas para modificar os dados. Uma para incluir reviews e outra para excluir.

 

 

Incluindo reviews

As “rotas” que geram alterações são chamadas de “mutations” (mutações).

Nestes casos, as operações podem falhar e portanto, é importante incluirmos um retorno padronizado, que vai indicar se um error ocorreu, se conseguimos concluir a operação com sucesso, etc.

Para isso, acesse o arquivo schema.graphql e inclua o seguinte tipo:

type ReviewMutationPayload {
    success: Boolean!
    errorMessage: String
    review: Review
}

No tipo acima retornamos (de forma obrigatória) um booleano que indica se houve ou não sucesso, uma mensagem de erro (opcional, pois só usaremos se  algo errado acontecer) e a review em si, que será retornada quando a operação der certo.

 

Vamos planejar: O que precisamos para criar uma review? O id do produto, o autor, a nota e conteúdo.

São 4 argumentos. Poderíamos simplesmente incluir todos na chamada da query, como fizemos com a query review ou product, mas seria melhor se organizássemos isso. Vamos então criar uma definição do tipo input (que é basicamente um type que serve como tipo de entrada):

input ReviewCreateInput {
    productId: ID!
    author: String!
    grade: Int!
    content: String!
}

Neste tipo, defino que receberemos os quatro argumentos de forma obrigatória (todos terminam com !).

O próximo passo é incluir a mutação para criação de uma review:

type Mutation {
    reviewCreate(newReviewData: ReviewCreateInput!): ReviewMutationPayload!
}

Nesta mutação, passamos os valores para criação da review e recebemos um resultado do tipo ReviewMutationPayload.

 

Antes de mapear a mutação, vamos fazer o resolver dela.

 

Resolver da mutação: reviewCreate

Dentro do pacote resolvers, crie outro chamado mutation_resolvers e nele crie o arquivo review_mutations.py e inclua o seguinte resolver:

# -*- coding: utf-8 -*-
from datetime import datetime
from ariadne import convert_kwargs_to_snake_case

from src.data import PRODUCTS, REVIEWS
from src.settings import LOGGER


@convert_kwargs_to_snake_case
def mutation_review_create_resolver(obj, info, new_review_data: dict) -> dict:
    LOGGER.info(f"Resolving: Mutation.reviewCreate | Query number={info.context.get('request_count')}")

    product_id = int(new_review_data["product_id"])
    new_review_data["product_id"] = product_id

    if not any(p for p in PRODUCTS if p.get('id') == product_id):
        return {
            "success": False,
            "error_message": f"No product found with id '{product_id}'"
        }

    new_review_id = max([r["id"] for r in REVIEWS]) + 1
    REVIEWS.append({
        "id": new_review_id,
        "date_added": datetime.utcnow(),
        **new_review_data
    })

    return {
        "success": True,
        "review": REVIEWS[-1]
    }

Este resolver ficou um pouco grande, mas ainda está bem simples. Note que o argumento new_review_data tem o mesmo nome que colocamos o schema (reviewCreate(newReviewData: ReviewCreateInput!)), porém utilizando  a convenção snake_case ao invés de camelCase e tudo funciona por causa do decorator convert_kwargs_to_snake_case. Sem ele, o nome do argumento teria que ser igual ao que foi definido no schema.

O que este resolver faz:

  • Log informando que o resolver está sendo executado;
  • Extraio o id do produto do argumento recebido e o converte para int;
  • Substituo o valor informado por argumento pelo próprio valor, mas convertido para int (tem maneiras mais limpas de se fazer isso? Sim, mas para deixar claro o que eu estava fazendo, deixei desta forma);
  • Confiro se o id de produto corresponde a algum produto existente. Se não, retorno uma mensagem de erro;
  • Gero um id para a nova review;
  • Adiciono uma nova review com os dados fornecidos;
  • Retorno indicador de sucesso, incluindo review que acabou de ser criada.

Você pode estar se perguntando: Você não deveria validar se o argumento new_review_data possui todas as propriedades que você precisa?

Bom, teoricamente não. No schema eu já informei que todas estas propriedades são obrigatórias, então elas vão existir. Pode ser que content tenha uma string vazia ou que grade (nota do produto) tenha um valor negativo, mas as propriedades estarão lá. (Se fosse uma aplicação real, certamente o valor das propriedades deveria ter sido validado, mas para este exemplo podemos fingir que o usuário nunca erra.)

 

O que falta para esta mutação  é mapeá-la no app.py:

from src.resolvers.mutation_resolvers.review_mutations import mutation_review_create_resolver
mutation = ObjectType("Mutation")
mutation.set_field("reviewCreate", mutation_review_create_resolver)

Depois vamos incluir a variável mutation na chamada do método make_executable_schema:

schema = make_executable_schema(
    type_defs, query, mutation, product, review, snake_case_fallback_resolvers
)

 

Agora para o teste: Vamos executar o nosso servidor GraphQL e incluir uma review para um produto que ainda não tem nenhuma:

mutation {
  reviewCreate(newReviewData:{
    productId:1
    author:"raccoon421"
    grade: 5
    content: "Great cable! Very sturdy, excellent craftsmanship!"
  }) {
    success
    errorMessage
    review {      
      product {
        name
        
      }
    }
  }
}

Na query acima, estamos passando o id 1 (produto que não tem reviews), depois os outros dados necessários para a review e do retorno eu peço o indicador de sucesso, a mensagem de erro e da review que foi criada, apenas o nome do produto correspondente.

O resultado desta mutação seria este:

/{
  "data": {
    "reviewCreate": {
      "errorMessage": null,
      "review": {
        "product": {
          "name": "USB Type-C cable - 3m"
        }
      },
      "success": true
    }
  }
}

 

Estamos quase no final deste livro tutorial! Falta apenas uma mutação: a que vai remover uma review.

 

Resolver da mutação: reviewDelete

O processo para esta mutação será bem parecido.

No arquivo schema.graphql, substitua o tipo Mutation pelo código abaixo:

type Mutation {
    reviewCreate(newReviewData: ReviewCreateInput!): ReviewMutationPayload!
    reviewDelete(reviewId: ID!): ReviewMutationPayload!
}

 

Como todos os tipos necessários já estão definidos, podemos passar para o resolver. Então abra o arquivo review_mutations.py e inclua o seguinte método:

@convert_kwargs_to_snake_case
def mutation_review_delete_resolver(obj, info, review_id: str) -> dict:
    LOGGER.info(f"Resolving: Mutation.reviewDelete | Query number={info.context.get('request_count')}")
    rev_id = int(review_id)
    filtered_reviews = [r for r in REVIEWS if r.get('id') == rev_id]
    if len(filtered_reviews) == 0:
        return {
            "success": False,
            "error_message": f"No product found with id '{review_id}'"
        }

    review = filtered_reviews[0]
    REVIEWS.remove(review)

    return {
        "success": True,
        "review": review
    }

O que este resolver faz é bem similar ao anterior: Primeiro eu confiro se o id passado aponta para alguma review, se não apontar, eu retorno um erro. Se apontar, apago a review e a retorno no corpo da resposta, juntamente com um indicador de sucesso.

Agora basta ir no app.py e mapear este novo resolver:

from src.resolvers.mutation_resolvers.review_mutations import mutation_review_delete_resolver
mutation.set_field("reviewDelete", mutation_review_delete_resolver)

 

Por último, vamos testar! A query abaixo  vai apagar a review com Id 4:

mutation {
  reviewDelete(reviewId:4) {
    success
    errorMessage
    review {
      author
      content
      grade
      dateAdded
      product {
        id
        name
      }
    }
  }
}

 

O retorno desta query será:

{
  "data": {
    "reviewDelete": {
      "errorMessage": null,
      "review": {
        "author": "beerGourmetPerson",
        "content": "This beer is pretty generic. It's not great, but it's not terrible. It's just kind of... there. If you're looking for a beer that won't offend anyone and will just get the job done, this is the one for you. But if you're looking for!",
        "dateAdded": "2022-07-07 01:20:44.603667",
        "grade": 3,
        "product": {
          "id": "3",
          "name": "Generic Beer"
        }
      },
      "success": true
    }
  }
}

Este resultado mostra que apagamos a review, mas também devolve informações sobre o produto ao qual ela pertencia.

 

 

Conclusões

Com este exemplo, espero que tenha ficado claro o grau de flexibilidade e benefícios que utilizar GraphQL pode trazer para o seu projeto. Com um schema bem definido e a criação de resolvers bem simples, conseguimos disponibilizar a quantidade exata de informações, de forma rápida e simples.

Para manter este exemplo o mais simples possível, acabei omitindo algumas validações, verificações e estruturas que devem existir em uma aplicação de verdade. Por exemplo, não existe uma identificação devida para cada usuário e qualquer um pode apagar qualquer review, independente dela ter sido criada pelo próprio usuário ou não. Também não incluí (no exemplo deste post) uma forma do usuário se autenticar, mas um exemplo mais completo está no repositório. São as pastas server_python e server_nodejs.

Se incluirmos os dataloaders, o problema do N+1 se resolve (ou ao menos é amenizado). Existe também a funcionalidade de subscription para utilizar funcionalidades de notificação push (atualizações baseadas em eventos), mas este é outro assunto que extrapola o que eu quis demonstrar aqui.

Mas então, será que o GraphQL veio para substituir o padrão REST? Provavelmente não. Vale lembrar aquele velho jargão “não existe bala de prata”, ou seja, não vamos resolver todos os nossos problemas simplesmente mudando de REST para GraphQL. É uma alternativa excelente, mas ambos tem sua aplicabilidade. Como tudo na tecnologia, esta escolha deve ser baseada nos requisitos do projeto e nas possibilidades que o cliente permite e não no que achamos mais interessante.

Então é isso. Mais de 7400 palavras depois (de acordo com o contador do WordPress), consegui chegar ao final do tutorial com as funcionalidades básicas do GraphQL.

 

Espero ter ajudado! 🙂

The following two tabs change content below.
Arquiteto de Software e Desenvolvedor Backend (quase Fullstack), geralmente trabalho com C#, PowerShell, Python, Golang, bash e Unity (esse é mais por hobby). Estou sempre buscando algo novo para aprender, adicionando novas ferramentas ao meu cinto de utilidades.
Posted in Dev, JavaScript, Python and tagged , , , , , , , .