-
Notifications
You must be signed in to change notification settings - Fork 1
GraphQL
Essa página se concentrará em explicar como está funcionando a implementação do GraphQL no projeto backend, bem como os casos de uso e como configurar o modelo do Django para ser integrado automaticamente ao GraphQL. Antes de tudo, só é possível termos o GraphQL neste projeto graças à biblioteca do Python graphene_django. Portanto, caso algo não esteja coberto por esta documentação em relação ao Graphene, consulte a documentação oficial da biblioteca.
Neste momento, os principais arquivos relacionados ao GraphQL no projeto estão na pasta
/backend/custom/
.
O schema é uma parte muito importante para o Graphene, pois nele definimos como nossos dados serão disponibilizados para o mundo, os contratos, os campos, as queries e mutations. Se você é novo neste assunto de GraphQL, aqui está uma boa introdução para se familiarizar: Introduction to GraphQL 🚀.
Continuando, o schema é o ponto inicial dessa jornada, e aqui neste projeto ele é criado de maneira automática através de um script. Com base em configurações definidas nos modelos do Django, conseguimos criar automaticamente as queries e mutations da nossa aplicação. De certo modo, isso é bom, mas ao mesmo tempo adiciona complexidade nesse parser que cria tudo de forma mágica. Abaixo segue o código de como estamos carregando o schema gerado no Graphene:
# backend/apps/schema.py
schema = build_schema(
applications=["account", "v1"],
extra_queries=[
APIQuery,
PaymentQuery,
],
extra_mutations=[
AccountMutation,
PaymentMutation,
],
)
Aqui está a explicação detalhada dos campos utilizados no código:
-
applications
: Este campo se refere a quais módulos/apps do Django queremos carregar os modelos no nosso schema. Caso no futuro seja criado algum outro app, basta adicioná-lo aqui nesta lista. -
extra_queries
: Aqui adicionamos queries que não são geradas automaticamente pelo script, mas que são necessárias para o funcionamento da aplicação. -
extra_mutations
: Funciona da mesma forma que o campo anterior, mas para mutations..
Você pode acessar o playground do GraphQL local em
http://localhost:8000/graphql/
após rodar o projeto backend.
O Graphene precisa se configurada na nossa aplicação Django para que possamos utilizá-lo. Abaixo está o código de como estamos configurando o Graphene no projeto:
# backend/settings/base.py
GRAPHENE = {
"SCHEMA": "backend.apps.schema.schema",
"MIDDLEWARE": [
"graphql_jwt.middleware.JSONWebTokenMiddleware",
],
"RELAY_CONNECTION_MAX_LIMIT": 1500,
}
Aqui está a explicação detalhada dos campos utilizados no código:
-
SCHEMA
: Aqui definimos o caminho para o schema que criamos anteriormente. -
MIDDLEWARE
: Aqui adicionamos o middleware dodjango-graphql-jwt
para que possamos utilizar JWT nas queries e mutations. -
RELAY_CONNECTION_MAX_LIMIT
: Este campo é opcional e define o limite de conexões que o Relay pode ter. O Relay é uma biblioteca que ajuda a lidar com a paginação no GraphQL.
A configuração RELAY_CONNECTION_MAX_LIMIT
: 1500 define um limite máximo de itens que podem ser retornados em uma única solicitação de uma conexão Relay em um sistema GraphQL dentro de um projeto Django. Isso ajuda a manter a eficiência do sistema e evita sobrecarregar o servidor com respostas muito grandes.
O script responsável por gerar o esquema do GraphQL é um pouco complexo, mas vou destacar seus principais pontos. Você pode encontrá-lo em backend/custom/graphql_auto.py
. Este script tem a função de carregar os modelos do Django e gerar automaticamente as consultas (queries) e as mutações. Ele está dividido em duas partes principais: query e mutation. No entanto, antes de entrar em detalhes sobre essas partes, vamos entender algumas classes personalizadas que são utilizadas no script:
As classes personalizadas podem ser encontradas no arquivo backend/custom/graphql_base.py
. Elas são utilizadas para facilitar a criação de queries e mutations no script. Como? Elas são implementações de classes que estendem as funcionalidades padrão do Graphene, pensando no contexto do nosso projeto. Abaixo estão as classes personalizadas que são utilizadas no script:
Objetivo:
PlainTextNode
é uma implementação personalizada de um nó Relay em um projeto Django com GraphQL. Ele é utilizado para criar e interpretar IDs globais únicos para objetos, essenciais para referenciar e manipular objetos de maneira consistente em um sistema GraphQL.
Métodos:
-
to_global_id(type, id)
:- Cria um ID global único concatenando o tipo e o ID do objeto com dois pontos (
:
). - Garante que cada objeto tenha um identificador único e global no sistema.
- Cria um ID global único concatenando o tipo e o ID do objeto com dois pontos (
-
from_global_id(global_id)
:- Divide um ID global em suas partes componentes (tipo e ID) retornando uma lista.
- Permite a identificação e manipulação do objeto original a partir de seu ID global.
Esses métodos são estáticos e facilitam a conversão entre IDs locais e globais, garantindo a integridade e a unicidade dos identificadores no sistema GraphQL.
Objetivo:
CountableConnection
é uma classe personalizada e abstrata que estende a classe Connection
em um projeto GraphQL. Ela proporciona funcionalidades adicionais para contagem de itens e arestas em uma conexão.
Métodos:
-
resolve_total_count(info, **kwargs)
:- Retorna o número total de itens na conexão.
-
resolve_edge_count(info, **kwargs)
:- Retorna o número total de arestas na conexão.
Essa classe é útil para incluir informações sobre a quantidade total de itens ou arestas em uma conexão GraphQL, especialmente em cenários de paginação.
Objetivo:
FileFieldScalar
é uma classe personalizada que estende a classe Scalar
e representa um campo de arquivo. Ela é utilizada para manipular e serializar arquivos em operações de consulta e mutação.
Métodos:
-
serialize(value)
:- Serializa o valor do campo de arquivo para um formato adequado para operações GraphQL.
-
parse_literal(node)
:- Analisa um nó literal para obter o valor do campo de arquivo.
-
parse_value(value)
:- Analisa um valor para obter o valor do campo de arquivo.
De volta ao script. Vou tentar me ater às regras principais e menos à implementação, pois o código pode sofrer alterações no futuro. No entanto, as regras devem se manter, a princípio. Dado todos os nomes dos apps que queremos carregar, o script vai iterar sobre cada app em busca dos modelos, sendo esses modelos os padrões do Django. Para cada modelo encontrado, o script vai criar uma query do Graphene com base nas configurações do modelo. Aqui estão algumas regras que o script segue para criar as queries:
Cada modelo analisado será convertido em um campo na classe Query. O modelo será representado por um PlainTextNode e, na meta, o PlainTextNode será incluído como interface. Além disso, uma CountableConnection será usada como classe de conexão para o modelo. No mesmo contexto, todos os campos de filtro serão criados.
Por padrão será criado dois fields na query root para cada modelo. Um chamado ModelNameNode onde ModelName será o nome do modelo por exemplo para um modelo chamado User
o field será UserNode
. E outra chamada all_ModelName onde ModelName será o nome do modelo por exemplo para um modelo chamado User
o field será allUsers
.
Nesta seção, vou focar nos campos de filtro, pois são essenciais para a funcionalidade do GraphQL. Os campos de filtro são criados com base nos campos do modelo. O script vai criar um campo de filtro para cada campo do modelo. No entanto, existem alguns campos que serão ignorados:
- Qualquer campo que tenha o nome "_field_status".
- Qualquer campo do tipo imagem.
- Qualquer campo do tipo JSON.
Por padrão os seguintes filtros são adicionados para todos os campos, a seguir um exemplo para o campo id
:
- id: Filtra os resultados para que o campo id corresponda exatamente ao valor fornecido.
- id_Isnull: Filtra os resultados para que o campo id seja nulo ou não nulo.
- id_In: Filtra os resultados para que o campo id esteja dentro da lista de valores fornecida.
Se o campo for uma "string", consideremos string os tipos CharField
e TextField
do django. Os seguintes filtros são adicionados. A seguir um exemplo para o campo name
:
- name_Icontains: Filtra os resultados para que o campo name contenha (case-insensitive) o valor fornecido.
- name_Istartswith: Filtra os resultados para que o campo name comece com (case-insensitive) o valor fornecido.
- name_Iendswith: Filtra os resultados para que o campo name termine com (case-insensitive) o valor fornecido.
Se o campo for do tipo "comparável", consideremos comparável os tipos: IntegerField
, FloatField
, DecimalField
, DateTimeField
, DateField
, TimeField
, DurationField
. Os seguintes filtros são adicionados. A seguir um exemplo para o campo age
:
- age_Lt: Filtra os resultados para que o campo age seja menor que o valor fornecido.
- age_Lte: Filtra os resultados para que o campo age seja menor ou igual ao valor fornecido.
- age_Lt: Filtra os resultados para que o campo age seja maior que o valor fornecido.
- age_Gte: Filtra os resultados para que o campo age seja maior ou igual ao valor fornecido.
- age_Range: Filtra os resultados para que o campo age esteja dentro do intervalo de valores fornecido (um array com dois elementos, representando o intervalo fechado).
Se o campo for do tipo "foreign_key", consideramos foreign_key os tipos: ForeignKey
, OneToOneField
, OneToOneRel
, ManyToManyField
, ManyToManyRel
, ManyToOneRel
. Nesse caso, o script vai criar um 'nested filter' para o campo. A seguir, um exemplo para o campo user
:
- user_Id: Filtra os resultados para que o campo user seja igual ao valor fornecido.
- user_Id_Isnull: Filtra os resultados para que o campo user seja nulo ou não nulo.
- user_Name_Icontains: Filtra os resultados para que o campo user contenha (case-insensitive) o valor fornecido.
- user_Name_Istartswith: Filtra os resultados para que o campo user comece com (case-insensitive) o valor fornecido.
Also the script will creat the default filters options, first
, last
, before
, after
, offset
.
- first: Limita o número de resultados retornados.
- last: Limita o número de resultados retornados.
- before: Retorna os resultados antes de um cursor específico.
- after: Retorna os resultados após um cursor específico.
- offset: Retorna os resultados após um offset específico.
Para que o script possa criar as queries e mutations automaticamente, é necessário configurar o modelo do Django de acordo com as regras estabelecidas. Aqui estão algumas dicas para configurar o modelo do Django para o GraphQL:
Nesse caso, estamos falando quais campos estarão disponíveis para filtragem no GraphQL no modelo pai. Por exemplo, se temos um modelo Area
e queremos apenas que o campo id
seja filtrável em um modelo pai, podemos fazer o seguinte:
class Area(BaseModel):
"""Area model"""
id = models.UUIDField(primary_key=True, default=uuid4)
slug = models.SlugField(unique=True)
name = models.CharField(max_length=255, blank=False, null=False)
graphql_nested_filter_fields_whitelist = ["id"]
# Rest of the model
Nesse caso o campo graphql_nested_filter_fields_whitelist é uma lista de campos que estarão disponíveis para filtragem no modelo pai, os demais campos não estarão disponíveis para filtragem (no caso do modelo pai). O modelo Area terá todos os campos disponíveis seguindo as regras do script, em suas próprias queries.
Se você deseja remover um campo específico de ser filtrável no GraphQL, você pode fazer o seguinte:
class Area(BaseModel):
"""Area model"""
id = models.UUIDField(primary_key=True, default=uuid4)
slug = models.SlugField(unique=True)
name = models.CharField(max_length=255, blank=False, null=False)
graphql_filter_fields_blacklist = ["slug"]
# Rest of the model
É possível configurar outras opções no modelo do Django para o GraphQL, como:
-
graphql_fields_whitelist
: Validação de campos que serão incluídos no GraphQL. -
graphql_fields_blacklist
: Validação de campos que serão excluídos do GraphQL. -
graphql_filter_fields_whitelist
: Validação de campos que serão incluídos nos filtros do GraphQL. -
graphql_filter_fields_blacklist
: Validação de campos que serão excluídos dos filtros do GraphQL. -
graphql_nested_filter_fields_whitelist
: Validação de campos que serão incluídos nos filtros aninhados do GraphQL. -
graphql_nested_filter_fields_blacklist
: Validação de campos que serão excluídos dos filtros aninhados do GraphQL.
É possível definir a visibilidade de um modelo no GraphQL, para que ele não seja incluído nas queries e mutations. Para isso, você pode adicionar o campo graphql_visible = False
no modelo do Django.
class RegistrationToken(BaseModel):
token = models.CharField(max_length=255, unique=True, default=uuid4)
created_at = models.DateTimeField(auto_now_add=True)
used_at = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
graphql_visible = False
# Restante do modelo
É possível definir regras de autorização para queries e mutations no GraphQL. Para isso, você pode adicionar o campo graphql_query_decorator
ou graphql_mutation_decorator
no modelo do Django.
Os possíveis valores para esses campos são as seguintes:
- anyone_required: Qualquer pessoa pode acessar a query/mutation.
- owner_required: Apenas o dono do objeto pode acessar a query/mutation.
Sobre o owner_required, as seguintes regras se aplicam:
- É possível definir se o campo pode ser usado por um anônimo ou não com
allow_anonymous
(default: False). - Super users sempre podem modificar qualquer campo.
- Staff users sempre podem modificar qualquer campo.
- Anonymous users são atorisados a criar um novo objeto, mas não modificar.
- Authenticated users é autorizado a modificar seu próprio objeto.
Esses decorators vem direto do módulo graphql_jwt
e são os seguintes:
- login_required: O usuário deve estar logado para acessar a query/mutation.
- user_passes_test: Um callable verifica se o usuário pode acessar a query/mutation, levantando uma exceção PermissionDenied se retornar False.
- permission_required: Verifica se o usuário possui uma permissão específica para acessar a query/mutation.
- staff_member_required: O usuário deve ser um membro da equipe (User.is_staff=True) para acessar a query/mutation.
- superuser_required: O usuário deve ser um superusuário (User.is_superuser=True) para acessar a query/mutation.
Sobre as mutações, o script segue um padrão semelhante ao das queries. Para cada modelo, o script vai criar uma mutação de criação/atualização e exclusão. Aqui estão algumas regras que o script segue para criar as mutações:
Para mutações de exclusão ele irá criar um campo chamado DeleteModelName
onde ModelName será o nome do modelo por exemplo para um modelo chamado User
o field será DeleteUser
. Apenas um campo de entrada será criado para a mutação de exclusão, o campo será o id
do modelo. A mutação irá verificar a existência do objeto e deletá-lo.
Para mutações de criação e atualização ele irá criar um campo chamado CreateUpdateModelName
onde ModelName será o nome do modelo por exemplo para um modelo chamado User
o field será CreateUpdateUser
. Todos os campos do modelo serão adicionados como argumentos para a mutação. A mutação irá verificar se o objeto existe e atualizá-lo, caso contrário, irá criar um novo objeto.
Todos os campos do modelo serão adicionados como argumentos para a mutação. Além disso, o script irá adicionar um campo de retorno para a mutação, que será o modelo atualizado ou criado em lowercase. Por exemplo, para um modelo chamado User
, o campo de retorno será user
.
Exemplo de uma mutação de criação/atualização para o modelo Area
:
mutation {
createUpdateArea(input: { id: "1", name: "New Name", slug: "new-slug" }) {
area {
id
name
slug
}
}
}