GraphQL é uma linguagem de consulta criada pelo Facebook em 2012 e lançada publicamente em 2015.
É considerada uma alternativa para arquiteturas REST, além de oferecer um serviço runtime para rodar comandos e consumir uma API.
GraphQL fornece uma descrição completa e compreensível dos dados da API, dá aos clientes o poder de solicitar exatamente o que precisam, facilita a evolução de APIs ao longo do tempo e permite ferramentas poderosas para desenvolvedores.
Em outras palavras, o GraphQL atua como uma camada intermediária entre o cliente e a fonte de dados (como um banco de dados ou serviços externos).
Ele interpreta as consultas feitas pelo cliente e se encarrega de buscar os dados necessários a partir das diversas fontes, combiná-los se necessário e, finalmente, retornar ao cliente apenas os dados solicitados.
REST
- Estrutura de Dados Fixa:
- As APIs REST têm estruturas de dados predefinidas e endpoints específicos para diferentes tipos de requisições (GET, POST, PUT, DELETE).
- Over-fetching ou Under-fetching de Dados:
- Em uma API REST, os clientes podem receber mais dados do que precisam (over-fetching) ou menos dados do que precisam (under-fetching), pois os endpoints fornecem um conjunto fixo de informações.
- Múltiplas Requisições para Dados Relacionados:
- Para obter dados relacionados, os clientes geralmente precisam fazer várias requisições a endpoints diferentes.
- Versionamento de Endpoints:
- Às vezes, é necessário criar versões diferentes da API para manter a compatibilidade com clientes existentes quando novas funcionalidades são adicionadas.
GraphQL
- Consulta Flexível:
- O cliente especifica exatamente quais dados ele precisa, evitando over-fetching ou under-fetching de dados.
- Um Único Endpoint para Consultas:
- Todas as consultas GraphQL são feitas para um único endpoint, geralmente via POST HTTP.
- Agregação de Dados de Múltiplas Fontes:
- GraphQL pode unificar dados de várias fontes (como diferentes bancos de dados) em uma única consulta.
- Não Exige Versionamento:
- GraphQL elimina a necessidade de versionamento de endpoints, pois os clientes podem solicitar apenas os novos campos ou recursos de que precisam.
Embora o GraphQL tenha muitas vantagens em relação às APIs REST, também possui algumas desvantagens em determinados contextos. Aqui estão algumas das desvantagens do GraphQL em comparação com APIs REST:
-
Curva de Aprendizado: Para equipes e desenvolvedores que já estão familiarizados com o modelo de API REST, pode haver uma curva de aprendizado ao adotar o GraphQL.
Isso pode exigir tempo para se acostumar com os conceitos e práticas do GraphQL. -
Complexidade Adicional na Implementação: A implementação de um servidor GraphQL pode ser mais complexa do que a de um servidor REST simples, especialmente ao lidar com cenários avançados, como controle de acesso e resolução de consultas complexas.
-
Potencial para Over-fetching de Dados: Embora o GraphQL ofereça flexibilidade na obtenção de dados, os clientes também têm o potencial de solicitar uma grande quantidade de informações, o que pode resultar em over-fetching (obter mais dados do que necessário) se não for gerenciado adequadamente.
-
Possibilidade de Consultas Ineficientes: Como os clientes têm controle sobre as consultas, existe a possibilidade de um cliente criar uma consulta ineficiente que resulte em muitas operações de leitura no banco de dados.
Isso requer um bom planejamento na modelagem dos dados e otimização das resoluções de consulta. -
Possibilidade de Expansão Excessiva de Tipos: Às vezes, a definição de muitos tipos e relações no GraphQL pode levar a uma complexidade excessiva no esquema, tornando-o difícil de manter e entender.
-
Requer Tratamento de Cache Personalizado: Enquanto algumas bibliotecas e ferramentas fornecem suporte para cache com GraphQL, a implementação de um sistema de cache eficaz pode exigir mais esforço e consideração em comparação com REST, que possui padrões de cache bem estabelecidos.
-
Desafios na Integração com Ferramentas de Terceiros: Alguns serviços e ferramentas podem ter integrações mais diretas com APIs REST do que com APIs GraphQL, o que pode exigir mais trabalho para integrar sistemas de terceiros.
A estrutura de dados no GraphQL é baseada em um modelo de grafo, o que significa que os dados são organizados em um conjunto de nós interconectados.
Essa abordagem facilita a representação e a navegação de dados complexos e relacionados.
No contexto do GraphQL, os principais elementos são:
Os nós representam entidades individuais no sistema, como um usuário, um post em um blog, um produto em um catálogo, etc.
Cada nó possui um identificador único que o distingue de outros nós.
Também podem ser comparados às classes em Programação Orientada a Objetos
Cada nó tem campos que representam as informações disponíveis para esse tipo de nó.
Por exemplo, um nó de usuário pode ter campos como id, name, email, etc.
Cada campo tem um nome e um tipo associado.
Campos podem ser comparados aos atributos das classes em Programação Orientada a Objetos
Os nós podem se relacionar uns com os outros através de campos que referenciam outros tipos de nós.
Esses relacionamentos são como arestas em um grafo e permitem a navegação entre diferentes entidades.
Por exemplo, um usuário pode ter um campo posts que retorna uma lista de publicações associadas a esse usuário.
O GraphQL define tipos de dados que representam os diferentes objetos disponíveis.
Existem duas classes de tipos principais:
-
Tipos Predefinidos:
- Tipos Escalares (Scalars): São tipos de dados simples que representam valores únicos. Eles são chamados de "escalares" porque não têm subcampos e representam valores indivisíveis.
- Int: Representa um número inteiro.
- Float: Representa um número decimal.
- String: Representa uma sequência de caracteres de texto.
- Boolean: Representa um valor verdadeiro ou falso.
- ID: Representa um identificador único, frequentemente usado para identificar objetos.
- Tipos Escalares Personalizados: São tipos de dados definidos pelo usuário para representar valores específicos ou formatos de dados que não são cobertos pelos tipos escalares predefinidos
No exemplo a seguir, criaremos um tipo escalarDataAtual
, que retornará a data atual:// index.js const { ApolloServer , gql } = require('apollo-server') const typeDefs = gql` scalar DataAtual type Query { horaAtual: DataAtual } ` const resolvers = { Query: { horaAtual() { return new Date } } } const server = new ApolloServer({ typeDefs, resolvers }) server.listen().then(({ url }) => { console.log(`Executando em ${url}`) })
- Tipos Especiais:
-
Query: Usado para definir operações de consulta.
- Schema
type Book { id: ID title: String author: String } type Query { getBook(id: ID!): Book listBooks: [Book] }
- Consumindo as queries
getBook
elistBooks
- Consulta simples
query { listBooks { title author } }
- Consulta com Argumentos
query { getBook(id: "123") { title author } }
- Consultas Aninhadas
query { getBook(id: "123") { title author } listBooks { title } }
- Consulta com Fragmento
fragment BookDetails on Book { title author } query { getBook(id: "123") { ...BookDetails } }
- Consulta com Alias
query { theBook: getBook(id: "123") { title author } }
- Consulta com Argumentos Variáveis
query GetBook($id: ID!) { getBook(id: $id) { title author } }
- Consulta simples
- Schema
-
Mutation: Usado para definir operações de modificação de dados no servidor.
- Schema
type Mutation { createUser(input: CreateUserInput!): User updateUser(id: ID!, input: UpdateUserInput!): User deleteUser(id: ID!): Boolean }
- Criando um Usuário
mutation { createUser(input: { name: "John Doe", email: "[email protected]" }) { id name email } }
- Atualizando um Usuário
mutation { updateUser(id: "123", input: { name: "Jane Doe", email: "[email protected]" }) { id name email } }
- Excluindo um Usuário
mutation { deleteUser(id: "123") }
- Usando Variáveis em Mutações
mutation UpdateUser($id: ID!, $name: String!, $email: String!) { updateUser(id: $id, input: { name: $name, email: $email }) { id name email } }
- Schema
-
Subscription: Usado para definir operações de assinatura para dados em tempo real. As assinaturas não são resolvidas no momento da execução de uma única consulta. Em vez disso, elas são tratadas de forma assíncrona, pois envolvem a transmissão de atualizações em tempo real para os clientes que estão inscritos.
- Schema
type Subscription { newPost: Post userLoggedIn: User }
- Subscrição para Novas Postagens: Esta assinatura permite que os clientes recebam atualizações sempre que uma nova postagem for criada. A resposta da assinatura conterá os campos id e title da nova postagem.
subscription { newPost { id title } }
- Subscrição para Logins de Usuários: Esta assinatura permite que os clientes recebam atualizações sempre que um usuário fizer login. A resposta da assinatura conterá os campos id e name do usuário que fez o login.
subscription { userLoggedIn { id name } }
- Schema
-
- Tipos Escalares (Scalars): São tipos de dados simples que representam valores únicos. Eles são chamados de "escalares" porque não têm subcampos e representam valores indivisíveis.
-
Tipos Não Predefinidos:
- Tipos Complexos:
- Object Types (Tipos de Objetos): Representam entidades específicas com um conjunto de campos. Eles podem conter campos escalares ou outros tipos complexos.
type User { id: ID! name: String! email: String! age: Int posts: [Post] }
- Interface Types (Tipos de Interfaces): Definem um conjunto de campos que podem ser implementados por objetos. Permitem a criação de tipos abstratos que são compartilhados por diferentes objetos.
interface Post { id: ID! title: String! author: String! createdAt: String! } type TextPost implements Post { id: ID! title: String! author: String! createdAt: String! content: String! } type ImagePost implements Post { id: ID! title: String! author: String! createdAt: String! imageUrl: String! } type VideoPost implements Post { id: ID! title: String! author: String! createdAt: String! videoUrl: String! }
- Union Types (Tipos de Uniões): Representam um valor que pode ser um de vários tipos diferentes. Permitem agrupar diferentes tipos em um único tipo.
union Content = TextPost | ImagePost | VideoPost type TextPost { id: ID! title: String! author: String! createdAt: String! content: String! } type ImagePost { id: ID! title: String! author: String! createdAt: String! imageUrl: String! } type VideoPost { id: ID! title: String! author: String! createdAt: String! videoUrl: String! }
- Enum Types (Tipos de Enumeração): Representam um conjunto específico de valores pré-definidos. São úteis quando um campo só pode ter um dos valores de uma lista predefinida, como representar o status de um pedido em um sistema de e-commerce por exemplo.
enum OrderStatus { PENDING PROCESSING SHIPPED DELIVERED CANCELLED } type Order { id: ID product: Product total_amount: Float status: OrderStatus }
- Input Types (Tipos de Entrada): Usados para passar dados como argumentos em operações de mutação. São semelhantes aos tipos de objeto, mas são usados de forma específica para entrada de dados.
input NewUserInput { name: String! email: String! password: String! age: Int }
- Object Types (Tipos de Objetos): Representam entidades específicas com um conjunto de campos. Eles podem conter campos escalares ou outros tipos complexos.
- Tipos Complexos:
Um campo pode ser uma lista de valores, o que significa que ele pode conter múltiplos itens do mesmo tipo. Por exemplo, um livro pode ter uma lista de comentários associados a ele ou uma lista de avaliações que seria uma lista de números.
type Comment {
user: String!
text: String!
}
type Book {
title: String!
author: String!
ratings: [Int!]!
comments: [Comment!]!
}
O ponto de exclamação (!) é usado para indicar que um campo é não nulo, ou seja, que ele sempre terá um valor e não pode ser nulo (null). Isso significa que quando um campo é marcado com um ponto de exclamação, ele é obrigatório e deve ser fornecido em qualquer consulta que solicite esse campo. Caso contrário, a consulta não será válida e o servidor retornará um erro.
type User {
id: ID!
name: String
email: String!
age: Int
}
Além de simplesmente solicitar um campo, os clientes GraphQL podem passar argumentos para personalizar o resultado. Por exemplo, ao solicitar os detalhes de um usuário, o cliente pode especificar qual usuário quer, usando um argumento como id.
- Argumentos Simples:
Query { usuario(id: 1) { id nome email } }
- Argumentos de Lista:
Query { users(ids: [123, 456, 789]) { name email } }
- Argumentos com Tipos Personalizados:
Query { search(query: "GraphQL", type: VIDEO) { title url } }
- Argumentos com Paginação:
Query { posts(first: 10, after: "cursor123") { title body } }
- Argumentos com Valores Padrão:
Query { user(id: 123, showEmail: true) { name email } }
- Argumentos com Argumentos de Entrada:
mutation { createUser(input: { name: "John Doe" email: "[email protected]" password: "password123" }) { id name email } }
Um fragmento é uma maneira de definir um conjunto de campos que podem ser reutilizados em diferentes partes de uma consulta. Isso permite uma organização mais limpa e modular de consultas complexas, facilitando a manutenção e a legibilidade do código.
fragment UserFields on User {
email
}
query GetUser($showEmail: Boolean!) {
user(id: 123) {
name
...UserFields
}
}
As diretivas permitem que o cliente especifique como os resultados devem ser manipulados pelo servidor. Elas fornecem uma forma de customizar o comportamento da consulta.
As diretivas são utilizadas para condicionalmente incluir ou excluir campos em uma consulta ou mutação, com base em certas condições. Elas oferecem uma maneira flexível de personalizar os resultados com base em lógica condicional.
-
@deprecated: Esta diretiva é usada para marcar um campo como obsoleto ou desencorajado.
type Person { name: String! age: Int! @deprecated(reason: "Use 'birthYear' instead") birthYear: Int! }
-
@include: Permite que você inclua ou exclua a execução de um campo com base em uma condição.
query GetUser($showEmail: Boolean!) { user(id: 123) { name email @include(if: $showEmail) } }
-
@skip: Permite que você pule a execução de um campo se a condição fornecida for verdadeira.
query GetUser($hideEmail: Boolean!) { user(id: 123) { name email @skip(if: $hideEmail) } }
-
@specifiedBy: Usada para indicar qual especificação define a implementação de um tipo ou interface.
scalar MyScalar @specifiedBy(url: "http://example.com/myscalar")
-
@external: Utilizada em esquemas federados para indicar que um campo é resolvido por um serviço externo.
extend type Product @key(fields: "id") { id: ID! @external name: String @external price: Float @external }
-
@requires: Usada em esquemas federados para especificar que um campo depende de outro campo para ser resolvido.
extend type Review @key(fields: "id") { id: ID! @external author: User @requires(fields: "id") body: String @external }
-
@provides: Usada em esquemas federados para especificar que um campo fornece um determinado tipo de informação.
extend type User @key(fields: "id") { id: ID! @external username: String @external @provides(fields: "email") email: String @external }
-
@connection: Usada para definir campos de conexão em tipos, especialmente em contextos de paginamento.
type User { id: ID! posts: [Post] @connection(key: "UserPosts") }
-
@stream: Usada para indicar que um campo retorna uma lista que pode ser transmitida em tempo real.
type Subscription { liveComments(postId: ID!): [Comment!]! @stream }
-
Diretivas em fragmentos
fragment UserFields on User { email @include(if: $showEmail) } query GetUser($showEmail: Boolean!) { user(id: 123) { name ...UserFields } }
-
Diretivas em mutações
input UpdateUserInput { id: ID! name: String email: String age: Int } mutation UpdateUser($input: UpdateUserInput!) { updateUser(input: $input) { id name email @include(if: $input.includeEmail) } }
Esquemas federados em GraphQL referem-se a uma abordagem arquitetônica que permite que diferentes partes de um esquema GraphQL sejam gerenciadas e servidas por sistemas independentes, conhecidos como serviços. Esses serviços podem ser desenvolvidos, mantidos e escalados de forma independente, o que facilita a construção e a evolução de grandes sistemas distribuídos.
O objetivo dos esquemas federados é lidar com a complexidade de grandes sistemas ao dividir a funcionalidade em serviços especializados. Cada serviço possui seu próprio esquema GraphQL e é responsável por resolver os campos correspondentes.
Para facilitar a comunicação entre os serviços, o GraphQL introduziu três diretivas específicas para esquemas federados:
- @key: Esta diretiva é usada para indicar como um tipo deve ser identificado globalmente. Um tipo com a diretiva @key deve fornecer um conjunto de campos que o identificam de forma exclusiva em todo o sistema
type Product @key(fields: "id") { id: ID! name: String }
- @external: Esta diretiva é usada para indicar que um campo é resolvido por um serviço externo. Ela informa ao sistema federado que o campo não é resolvido localmente.
extend type Product @key(fields: "id") { id: ID! @external name: String @external price: Float @external }
- @requires e @provides: Essas diretivas são usadas para definir dependências entre campos de diferentes tipos. @requires indica que um campo depende da existência de outro campo para ser resolvido. @provides indica que um campo fornece um determinado tipo de informação.
extend type Review @key(fields: "id") { id: ID! @external author: User @requires(fields: "id") body: String @external }
Os esquemas federados são particularmente úteis em ambientes onde diferentes equipes ou serviços são responsáveis por diferentes partes da aplicação. Eles permitem escalar e evoluir cada serviço de forma independente, sem a necessidade de coordenação entre as equipes para fazer alterações no esquema.
Além disso, os esquemas federados facilitam a construção de sistemas distribuídos complexos, como plataformas de comércio eletrônico, redes sociais e outras aplicações de grande escala, ao permitir a composição de dados a partir de múltiplos serviços.
São funções que determinam como os dados são obtidos ou modificados em um esquema.
Eles são responsáveis por "resolver" os campos especificados em uma consulta para retornar os dados correspondentes.
Cada campo em um esquema deve ter um resolver associado a ele. Quando uma consulta é feita, o GraphQL chama os resolvers para cada campo na consulta para obter os dados correspondentes. Os resolvers são essenciais para conectar o esquema à fonte de dados real (como um banco de dados, uma API externa ou qualquer outra fonte de dados).
Cada campo em um esquema (seja um dado de tipo Query, Mutation ou qualquer outro tipo) deve ter um resolver associado a ele.
Um resolver é uma função que aceita quatro argumentos: parent
, args
, context
e info
.
- parent: Também conhecido como "objeto pai", é o objeto que contém o resultado da resolução do campo pai.
Em resoluções de campos de topo (como os de Query), esse argumento geralmente não é utilizado. - args: São os argumentos passados na consulta para este campo específico.
Por exemplo, em uma consulta getUser(id: "123"), o argumento id seria passado para o resolver. - context: É um objeto que pode ser usado para compartilhar informações entre diferentes resolvers em uma única requisição.
É útil para coisas como autenticação, controle de acesso ou conexão com banco de dados. - info: Contém informações sobre a execução da consulta, como a seleção de campos feita pelo cliente.
- Mapeamento de Campos:
Cada campo no esquema tem um resolver correspondente.
Por exemplo, se você tem um campo salario em um tipo Usuario, você teria um resolver para salario. - Execução de Resolvers: Quando uma consulta é feita, o GraphQL começa pela raiz da consulta (geralmente o tipo Query) e executa os resolvers correspondentes para cada campo na árvore da consulta.
- Hierarquia de Chamadas: Os resolvers são chamados em uma hierarquia, começando do nível mais alto da árvore de consulta até os níveis mais baixos. Cada resolver é responsável por fornecer os dados para o campo associado.
- Tratamento de Relacionamentos: Em resolvers para campos que representam relacionamentos (como um campo author que se refere a outro tipo User), o resolver pode fazer uma chamada a uma fonte de dados para recuperar os dados relacionados.
- Retorno de Dados: Os resolvers devem retornar os dados correspondentes ao campo. Por exemplo, para o campo title, o resolver deve retornar o título do livro.
- Recursividade: Se um campo possui subcampos, o GraphQL chama os resolvers correspondentes para esses subcampos, e assim por diante, até que todos os dados sejam resolvidos.
- Root resolvers:
- Query: Este resolver é responsável por resolver os campos de consulta no esquema GraphQL. Ele define como os dados são obtidos quando uma consulta é feita.
Suponha que temos um esquema com uma consulta getUser para obter informações de um usuário.
O resolver para a consulta getUser pode ser implementado de maneira que cada post tem um authorId que corresponde ao id de um usuário.const users = [ { id: '1', name: 'John Doe', email: '[email protected]' }, { id: '2', name: 'Jane Doe', email: '[email protected]' } ]; const typeDefs = gql` type Query { getUser(id: ID!): User } type User { id: ID! name: String! email: String! } ` const resolvers = { Query: { getUser: (parent, args) => { return users.find(user => user.id === args.id); } } };
- Mutation: Semelhante ao Query, mas usado para resolver operações de mutação, como inserção, atualização e exclusão de dados.
O resolvercreateUser
recebe os argumentos name e email e cria um novo usuário com um ID único.const users = [ { id: '1', name: 'John Doe', email: '[email protected]' }, { id: '2', name: 'Jane Doe', email: '[email protected]' } ]; const typeDefs = gql` type User { id: ID! name: String! email: String! } type Mutation { createUser(name: String!, email: String!): User } ` const resolvers = { Query: { /* ... */ }, Mutation: { createUser: (parent, args) => { const newUser = { id: String(users.length + 1), ...args }; users.push(newUser); return newUser; } } };
- Query: Este resolver é responsável por resolver os campos de consulta no esquema GraphQL. Ele define como os dados são obtidos quando uma consulta é feita.
- Field resolvers: São usados para resolver campos específicos em tipos de objetos. Por exemplo, se tivermos um tipo
User
com um campoposts
, o field resolver paraposts
determinará como os posts associados a esse usuário são recuperados.const users = [ { id: '1', name: 'John Doe', email: '[email protected]' }, { id: '2', name: 'Jane Doe', email: '[email protected]' } ]; const posts = [ { id: '1', title: 'First Post', content: 'Content of the first post', authorId: '1' }, { id: '2', title: 'Second Post', content: 'Content of the second post', authorId: '2' } ]; const typeDefs = gql` type User { id: ID! name: String! email: String! posts: [Post] } type Post { id: ID! title: String! content: String! authorId: ID! } ` const resolvers = { User: { posts: (parent) => { return posts.filter(post => post.authorId === parent.id); } } };
- Resolver de tipo de objeto (Object Type Resolvers): Em esquemas complexos, pode haver tipos de objetos que têm diferentes implementações ou fontes de dados. Resolver de tipo de objeto é usado para determinar como resolver um tipo de objeto em particular.
Suponha que temos dois tipos de objetos diferentes,
Dog
eCat
, e queremos resolver um camposound
para cada um deles.
Os resolvers de tipo de objeto determinarão como resolver o campo sound para cada tipo.const typeDefs = gql` type Dog { sound: String } type Cat { sound: String } ` const resolvers = { Dog: { sound: () => 'Woof!' }, Cat: { sound: () => 'Meow!' } };
- Resolver de união (Union Resolvers): Uniões em GraphQL permitem que um campo possa ter diferentes tipos, dependendo do contexto. Por exemplo, uma consulta pode retornar uma lista de animais, onde cada animal pode ser um tipo diferente (cachorro, gato, etc.). O resolver de união é usado para determinar como resolver cada tipo dentro da união.
Suponha que temos uma união chamadaAnimal
que pode ser umDog
ou umCat
. Precisamos resolver o campo sound para a união.
O resolver de união irá determinar como resolver o camposound
para cada tipo dentro da união.const typeDefs = gql` union Animal = Dog | Cat type Dog { sound: String } type Cat { sound: String } ` const resolvers = { Animal: { __resolveType(animal) { if (animal.sound === 'Woof!') { return 'Dog'; } else if (animal.sound === 'Meow!') { return 'Cat'; } return null; } } };
Os tópicos estudados neste capítulo serão implementados em uma aplicação em NodeJS, para isso será necessário a instalação do NodeJS e NPM, com a seguinte estrutura:
- schema-query/
- index.js
- package.json
// packge.json
{
"name": "schema-query",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon"
},
"keywords": [],
"author": "Italo Fasanelli",
"license": "ISC",
"dependencies": {
"apollo-server": "3.12.1",
"graphql": "16.8.1"
},
"devDependencies": {
"nodemon": "3.0.1"
}
}
// index.js
const { ApolloServer , gql } = require('apollo-server')
const typeDefs = gql`
type Query {
ola: String!
}
`
const resolvers = {
Query: {
ola() {
return 'Olá mundo!'
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => {
console.log(`Executando em ${url}`)
})
Schema: (Type Definitions) refere-se à definição dos tipos de dados que podem ser consultados ou manipulados através da API GraphQL.
É uma parte crucial do esquema GraphQL.
Exemplos de typeDefs
em Schema Definition Language (SDL):
type Query {
hello: String
}
type Mutation {
createUser(name: String, email: String): User
}
type User {
id: ID
name: String
email: String
}
No terminal, dentro do diretório do projeto schema-query
, deve-se executar npm i
para a instalação das dependências.
Em seguida executar o comando npm start
para rodar a aplicação.
Então a aplicação ganhará mais alguns arquivos e ficará:
- schema-query/
- node_modules/
- index.js
- package-lock.json
- package.json
Ao executar a query ola
obtemos a resposta definida:
Crie um tipo Produto
com os campos nome
(obrigatório), preco
(obrigatório), desconto
, precoComDesconto
(resolver) e uma consulta produtoEmDestaque
.
// index.js
const { ApolloServer , gql } = require('apollo-server')
const typeDefs = gql`
type Produto {
id: ID
nome: String!
preco: Float
desconto: Float
precoComDesconto: Float
}
type Query {
produtoEmDestaque: [Produto]
}
`
const resolvers = {
Produto: {
precoComDesconto(produto) {
if (produto.desconto) {
return (produto.preco * (1 - produto.desconto)).toFixed(2)
}
return produto.preco
}
},
Query: {
produtoEmDestaque() {
return [
{
id: 1,
nome: 'Maçã',
preco: 5.99,
desconto: 0.15
},
{
id: 2,
nome: 'Banana',
preco: 6.99,
desconto: 0.2
},
{
id: 3,
nome: 'Tomate',
preco: 6.99,
desconto: null
}
]
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => {
console.log(`Executando em ${url}`)
})
Crie um tipo Usuario
com os campos nome
, idade
, email
e uma consulta usuario
passando o argumento id
.
// index.js
const { ApolloServer , gql } = require('apollo-server')
const usuarios = [
{
id: 1,
nome: 'Italo',
idade: 36,
email: '[email protected]'
},
{
id: 2,
nome: 'Francisco',
idade: 0,
email: '[email protected]'
},
{
id: 3,
nome: 'Caetano',
idade: 0,
email: '[email protected]'
}
]
const typeDefs = gql`
type Usuario {
id: ID
nome: String!
idade: Int
email: String
}
type Query {
usuario(id: ID): Usuario
}
`
const resolvers = {
Query: {
usuario(_, args) {
const selecionado = usuarios.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => {
console.log(`Executando em ${url}`)
})
Crie um tipo Perfil
com os campos id
e nome
e duas consultas perfis
e perfil
passando o argumento id
.
// index.js
const { ApolloServer , gql } = require('apollo-server')
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
const typeDefs = gql`
type Perfil {
id: ID
nome: String!
}
type Query {
perfis: [Perfil]
perfil(id: ID): Perfil
}
`
const resolvers = {
Query: {
perfis() {
return perfis
},
perfil(_, { id }) {
const selecionado = perfis.filter(p => p.id == id)
return selecionado ? selecionado[0] : null
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => {
console.log(`Executando em ${url}`)
})
// index.js
const { ApolloServer , gql } = require('apollo-server')
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
const usuarios = [
{
id: 1,
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1
},
{
id: 2,
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2
},
{
id: 3,
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2
}
]
const typeDefs = gql`
type Usuario {
id: ID
nome: String!
idade: Int
email: String
perfil: Perfil
}
type Perfil {
id: ID
nome: String!
}
type Query {
usuarios: [Usuario]
}
`
const resolvers = {
Usuario: {
perfil(usuario) {
const selecionados = perfis.filter(p => p.id == usuario.perfil_id)
return selecionados ? selecionados[0] : null
}
},
Query: {
usuarios() {
return usuarios
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => {
console.log(`Executando em ${url}`)
})
// index.js
const { ApolloServer , gql } = require('apollo-server')
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
const usuarios = [
{
id: 1,
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1
},
{
id: 2,
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2
},
{
id: 3,
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2
}
]
const typeDefs = gql`
type Usuario {
id: ID
nome: String!
idade: Int
email: String
perfil: Perfil
}
type Perfil {
id: ID
nome: String!
}
type Query {
usuarios: [Usuario]
usuario(id: ID): Usuario
}
`
const resolvers = {
Usuario: {
perfil(usuario) {
const selecionados = perfis.filter(p => p.id == usuario.perfil_id)
return selecionados ? selecionados[0] : null
}
},
Query: {
usuarios() {
return usuarios
},
usuario(_, { id }) {
const selecionados = usuarios.filter(u => u.id == id)
return selecionados ? selecionados[0] : null
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => {
console.log(`Executando em ${url}`)
})
Começaremos neste capítulo a organizar o código separando em diversos arquivos tudo o que atualmente está no index.js
.
Neste momento temos o projeto composto da seguinte maneira:
- schema-query/
- node_modules/
- index.js
- package-lock.json
- package.json
Onde o index.js
:
// index.js
const { ApolloServer , gql } = require('apollo-server')
const produtos = [
{
id: 1,
nome: 'Maçã',
preco: 5.99,
desconto: 0.15
},
{
id: 2,
nome: 'Banana',
preco: 6.99,
desconto: 0.2
},
{
id: 3,
nome: 'Tomate',
preco: 6.99,
desconto: null
}
]
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
const usuarios = [
{
id: 1,
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1
},
{
id: 2,
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2
},
{
id: 3,
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2
}
]
const typeDefs = gql`
scalar DataAtual
type Perfil {
id: ID
nome: String!
}
type Produto {
id: ID
nome: String!
preco: Float
desconto: Float
precoComDesconto: Float
}
type Usuario {
id: ID
nome: String!
email: String!
idade: Int
salario: Float
vip: Boolean
perfil: Perfil
}
type Query {
ola: String!
horaAtual: DataAtual
usuarioLogado: Usuario
produtoEmDestaque: [Produto]
perfis: [Perfil]
perfil(id: ID): Perfil
usuarios: [Usuario]
usuario(id: ID): Usuario
}
`
const resolvers = {
Produto: {
precoComDesconto(produto) {
if (produto.desconto) {
return (produto.preco * (1 - produto.desconto)).toFixed(2)
}
return produto.preco
}
},
Usuario: {
salario(usuario) {
return usuario.salario_real
},
perfil(usuario) {
const selecionados = perfis.filter(p => p.id == usuario.perfil_id)
return selecionados ? selecionados[0] : null
}
},
Query: {
ola() {
return 'Olá mundo!'
},
horaAtual() {
return new Date
},
usuarioLogado() {
return {
id: 1,
nome: 'Italo',
email: '[email protected]',
idade: 36,
salario_real: 14999.99,
vip: true,
}
},
produtoEmDestaque() {
return produtos
},
perfis() {
return perfis
},
perfil(_, args) {
const selecionado = perfis.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
},
usuarios() {
return usuarios
},
usuario(_, { id }) {
const selecionados = usuarios.filter(u => u.id == id)
return selecionados ? selecionados[0] : null
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
})
server.listen().then(({ url }) => {
console.log(`Executando em ${url}`)
})
Para começar a organização, vamos separar o schema
do projeto criando um diretório de mesmo nome com um arquivo index.graphql
, um arquivo para cada type object
(Perfil, Produto e Usuario) e um para a query object
:
- schema-query/
- node_modules/
-schema/
- index.graphql
- Usuario.graphql
- Perfil.graphql
- Produto.graphql
- Query.graphql
- index.js
- package-lock.json
- package.json
// schema/index.graphql
# import Usuario from './Usuario.graphql'
# import Perfil from './Perfil.graphql'
# import Produto from './Produto.graphql'
# import Query from './Query.graphql'
scalar DataAtual
// schema/Usuario.graphql
type Usuario {
id: ID
nome: String!
email: String!
idade: Int
salario: Float
vip: Boolean
perfil: Perfil
}
// schema/Perfil.graphql
type Perfil {
id: ID
nome: String!
}
// schema/Produto.graphql
type Produto {
id: ID
nome: String!
preco: Float
desconto: Float
precoComDesconto: Float
}
// schema/Query.graphql
type Query {
ola: String!
horaAtual: DataAtual
usuarioLogado: Usuario
produtoEmDestaque: [Produto]
perfis: [Perfil]
perfil(id: ID): Perfil
usuarios: [Usuario]
usuario(id: ID): Usuario
}
// index.js
const { ApolloServer, gql } = require('apollo-server');
const { loadSchemaSync } = require('@graphql-tools/load')
const { GraphQLFileLoader } = require('@graphql-tools/graphql-file-loader')
const produtos = [
{
id: 1,
nome: 'Maçã',
preco: 5.99,
desconto: 0.15
},
{
id: 2,
nome: 'Banana',
preco: 6.99,
desconto: 0.2
},
{
id: 3,
nome: 'Tomate',
preco: 6.99,
desconto: null
}
]
const usuarios = [
{
id: 1,
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1
},
{
id: 2,
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2
},
{
id: 3,
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2
}
]
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
const resolvers = {
Query: {
ola: () => 'Olá, Mundo!',
horaAtual: () => new Date(),
usuarioLogado: () => ({ id: '1', nome: 'Usuário Teste', email: '[email protected]' }),
produtoEmDestaque: () => (produtos),
perfis: () => perfis,
perfil(_, args) {
const selecionado = perfis.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
},
usuarios: () => usuarios,
usuario(_, args) {
const selecionado = usuarios.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
}
},
Produto: {
precoComDesconto(produto) {
if (produto.desconto) {
return (produto.preco * (1 - produto.desconto)).toFixed(2)
}
return produto.preco
}
},
Usuario: {
perfil(usuario) {
const selecionados = perfis.filter(p => p.id == usuario.perfil_id)
return selecionados ? selecionados[0] : null
}
},
};
const typeDefs = loadSchemaSync("./**/*.graphql", {
loaders: [new GraphQLFileLoader()]
})
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Servidor pronto em ${url}`);
});
Note que importamos os arquivos .graphql
utilizando dois métodos (loadSchemaSync
e GraphQLFileLoader
) de duas bibliotecas (@graphql-tools/load
e
@graphql-tools/graphql-file-loader
) e atribuímos à constante typeDefs
.
E para efetivar as importações dos arquivos precisaremos instalar as duas bibliotecas com os comandos:
npm i -s @graphql-tools/load
npm i -s @graphql-tools/graphql-file-loader
Para organização dos dados hard coded
criaremos um diretório data
com o arquivo db.js
e moveremos os dados para ele.
Para a separação dos resolvers, criaremos o diretório resolvers
, com os arquivos: index.js
, Produto.js
, Query.js
e Usuario.js
.
O projeto ficará:
- schema-query/
- data/
- db.js
- node_modules/
- resolvers/
- index.js
- Produto.js
- Query.js
- Usuario.js
-schema/
- index.graphql
- usuario.graphql
- produto.graphql
- query.graphql
- index.js
- package-lock.json
- package.json
// data/db.js
const produtos = [
{
id: 1,
nome: 'Maçã',
preco: 5.99,
desconto: 0.15
},
{
id: 2,
nome: 'Banana',
preco: 6.99,
desconto: 0.2
},
{
id: 3,
nome: 'Tomate',
preco: 6.99,
desconto: null
}
]
const usuarios = [
{
id: 1,
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1,
vip: true,
salario: 10000
},
{
id: 2,
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2,
vip: true,
salario: 0
},
{
id: 3,
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2,
vip: true,
salario: 0
}
]
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
module.exports = { usuarios, perfis, produtos }
// resolvers/index.js
const Query = require('./Query')
const Produto = require('./Produto')
const Usuario = require('./Usuario')
module.exports = { Query, Produto, Usuario }
// resolvers/Produto.js
module.exports = {
precoComDesconto(produto) {
if (produto.desconto) {
return (produto.preco * (1 - produto.desconto)).toFixed(2)
}
return produto.preco
}
}
// resolvers/Query.js
const { perfis, produtos, usuarios } = require('../data/db')
module.exports = {
ola: () => 'Olá, Mundo!',
horaAtual: () => new Date(),
usuarioLogado: () => ({ id: '1', nome: 'Usuário Teste', email: '[email protected]' }),
produtoEmDestaque: () => (produtos),
perfis: () => perfis,
perfil(_, args) {
const selecionado = perfis.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
},
usuarios: () => usuarios,
usuario(_, args) {
const selecionado = usuarios.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
}
}
// resolvers/Usuario.js
const { perfis } = require('../data/db')
module.exports = {
perfil(usuario) {
const selecionados = perfis.filter(p => p.id == usuario.perfil_id)
return selecionados ? selecionados[0] : null
}
}
Com os resolvers
movidos do arquivo index.js
do projeto, devemos importar o novo diretório.
// index.js
const { ApolloServer, gql } = require('apollo-server');
const { loadSchemaSync } = require('@graphql-tools/load')
const { GraphQLFileLoader } = require('@graphql-tools/graphql-file-loader')
const resolvers = require('./resolvers')
const typeDefs = loadSchemaSync("./**/*.graphql", {
loaders: [new GraphQLFileLoader()]
})
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Servidor pronto em ${url}`);
});
// schema/Usuario.graphql
enum StatusUsuario {
ATIVO
INATIVO
BLOQUEADO
}
type Usuario {
id: ID
nome: String!
email: String!
idade: Int
salario: Float
vip: Boolean
perfil: Perfil
status: StatusUsuario
}
// data/db.js
const usuarios = [
{
id: 1,
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1,
status: 'ATIVO',
vip: true,
salario: 10000
},
{
id: 2,
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2,
status: 'INATIVO',
vip: true,
salario: 0
},
{
id: 3,
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2,
status: 'BLOQUEADO',
vip: true,
salario: 0
}
]
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
module.exports = { usuarios, perfis, produtos }
// schema/Usuario.graphql
type Perfil {
id: ID
nome: String
}
enum StatusUsuario {
ATIVO
INATIVO
BLOQUEADO
}
type Usuario {
id: ID
nome: String!
email: String!
idade: Int
salario: Float
vip: Boolean
perfil: Perfil
status: StatusUsuario
}
input UsuarioInput {
nome: String
email:String
idade: Int
}
// schema/Mutation.graphql
type Mutation {
novoUsuario(
dados: UsuarioInput!
): Usuario!
}
// resolvers/Mutation.js
const { usuarios, proximoId } = require('../data/db')
const { emailExistente, validarEmail } = require('../validations/email_validator')
module.exports = {
novoUsuario(_, { dados }) {
if (emailExistente(usuarios, dados.email) || validarEmail(dados.email)) {
throw new Error('E-mail já cadastrado ou está incorreto!')
}
const novo = {
id: proximoId(),
...dados,
perfil_id: 1,
status: 'ATIVO'
}
usuarios.push(novo)
return novo
}
}
// schema/Usuario.graphql
type Perfil {
id: ID
nome: String
}
enum StatusUsuario {
ATIVO
INATIVO
BLOQUEADO
}
type Usuario {
id: ID
nome: String!
email: String!
idade: Int
salario: Float
vip: Boolean
perfil: Perfil
status: StatusUsuario
}
input UsuarioFiltro {
id: Int
email:String
}
// schema/Mutation.graphql
type Mutation {
excluirUsuario(
filtro: UsuarioFiltro!
): Usuario
}
// resolvers/Mutation.js
const { usuarios, proximoId } = require('../data/db')
function indiceUsuario(filtro) {
if(!filtro) return -1
const { id, email } = filtro
if(id) {
return usuarios.findIndex(u => u.id == id)
} else if(email) {
return usuarios.findIndex(u => u.email == email)
}
return -1
}
module.exports = {
excluirUsuario(_, { filtro }) {
const i = indiceUsuario(filtro)
if(i < 0) return null
const excluidos = usuarios.splice(i, 1)
return excluidos ? excluidos[0] : null
}
}
Mutations são operações utilizadas para modificar os dados no servidor. Enquanto as queries são usadas para buscar dados, as mutations são usadas para criar, atualizar ou deletar dados.
As mutations são especialmente importantes em sistemas que seguem os princípios de CRUD (Create, Read, Update, Delete), pois elas permitem a alteração dos dados no servidor.
A seguir serão exemplificadas mutations de diversos tipos de operações com validações:
mutation
- data/
- db.js
- node_modules/
- resolvers/
- index.js
- Mutatios.js
- Query.js
- Usuario.js
- schema/
- index.graphql
- Mutation.graphql
- Perfil.graphql
- Query.graphql
- Usuario.graphql
- validations/
- email_validator.js
- index.js
- package-lock.json
- package.json
// data/db.js
let id = 1
function proximoId() {
return id++
}
const usuarios = [
{
id: proximoId(),
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1,
status: 'ATIVO',
vip: true,
salario: 10000
},
{
id: proximoId(),
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2,
status: 'INATIVO',
vip: true,
salario: 0
},
{
id: proximoId(),
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2,
status: 'BLOQUEADO',
vip: true,
salario: 0
}
]
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
module.exports = { usuarios, perfis, proximoId }
// resolvers/index.js
const Query = require('./Query')
const Usuario = require('./Usuario')
const Mutation = require('./Mutation')
module.exports = { Query, Usuario, Mutation }
// resolvers/Query.js
const { perfis, produtos, usuarios } = require('../data/db')
module.exports = {
perfis: () => perfis,
perfil(_, args) {
const selecionado = perfis.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
},
usuarios: () => usuarios,
usuario(_, args) {
const selecionado = usuarios.filter(u => u.id == args.id)
return selecionado ? selecionado[0] : null
}
}
// resolvers/Usuario.js
const { perfis } = require('../data/db')
module.exports = {
perfil(usuario) {
const selecionados = perfis.filter(p => p.id == usuario.perfil_id)
return selecionados ? selecionados[0] : null
}
}
// schema/index.graphql
# import Usuario from './Usuario.graphql'
# import Perfil from './Perfil.graphql'
# import Query from './Query.graphql'
# import Mutation from './Mutation.graphql'
// schema/Perfil.graphql
type Perfil {
id: ID
nome: String!
}
// schema/Query.graphql
type Query {
perfis: [Perfil]
perfil(id: ID): Perfil
usuarios: [Usuario]
usuario(id: ID): Usuario
}
// schema/Usuario.graphql
type Perfil {
id: ID
nome: String
}
enum StatusUsuario {
ATIVO
INATIVO
BLOQUEADO
}
type Usuario {
id: ID
nome: String!
email: String!
idade: Int
salario: Float
vip: Boolean
perfil: Perfil
status: StatusUsuario
}
// validations/email_validator.js
function emailExistente(usuarios, email) {
usuarios.some(u => u.email === email)
}
function validarEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
module.exports = { emailExistente, validarEmail }
// index.js
const { ApolloServer, gql } = require('apollo-server');
const { loadSchemaSync } = require('@graphql-tools/load')
const { GraphQLFileLoader } = require('@graphql-tools/graphql-file-loader')
const resolvers = require('./resolvers')
const typeDefs = loadSchemaSync("./**/*.graphql", {
loaders: [new GraphQLFileLoader()]
})
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Servidor pronto em ${url}`);
});
// schema/Mutation.graphql
type Mutation {
novoUsuario(
nome: String
email: String
idade: Int
): Usuario!
}
// resolvers/Mutation.js
const { usuarios, proximoId } = require('../data/db')
const { emailExistente, validarEmail } = require('../validations/email_validator')
module.exports = {
novoUsuario(_, args) {
if (emailExistente(usuarios, args.email) || validarEmail(args.email)) {
throw new Error('E-mail já cadastrado ou incorreto!')
}
const novo = {
id: proximoId(),
...args,
perfil_id: 1,
status: 'ATIVO'
}
usuarios.push(novo)
return novo
}
}
// schema/Mutation.graphql
type Mutation {
excluirUsuario(id: ID): Usuario
}
// resolvers/Mutation.js
const { usuarios, proximoId } = require('../data/db')
module.exports = {
excluirUsuario(_, { id }) {
const i = usuarios.findIndex(u => u.id == id)
if(i < 0) return null
const excluidos = usuarios.splice(i, 1)
return excluidos ? excluidos[0] : null
}
}
// schema/Mutation.graphql
type Mutation {
alterarUsuario(
id: ID
nome: String
email: String
idade: Int
): Usuario
}
// resolvers/Mutation.js
const { usuarios, proximoId } = require('../data/db')
module.exports = {
alterarUsuario(_, args) {
const i = usuarios.findIndex(u => u.id == args.id)
if(i < 0) return null
const usuario = { ...usuarios[i], ...args }
usuarios.splice(i, 1, usuario)
return usuario
}
}
obj1 = { nome: 'Italo', numero: 123 }
obj2 = { sobrenome: 'Fasanelli', numero: 456 }
obj3 = { ...obj1, ...obj2 }
obj3
// {nome: "Italo", numero: 456, sobrenome: "Fasanelli"}
Implementar Input na alteração de usuário
// schema/Usuario.graphql
type Perfil {
id: ID
nome: String
}
enum StatusUsuario {
ATIVO
INATIVO
BLOQUEADO
}
type Usuario {
id: ID
nome: String!
email: String!
idade: Int
salario: Float
vip: Boolean
perfil: Perfil
status: StatusUsuario
}
input UsuarioInput {
nome: String
email:String
idade: Int
}
input UsuarioFiltro {
id: Int
email:String
}
// schema/Mutation.graphql
type Mutation {
alterarUsuario(
filtro: UsuarioFiltro!
dados: UsuarioInput!
): Usuario
}
// resolvers/Mutation.js
const { usuarios, proximoId } = require('../data/db')
function indiceUsuario(filtro) {
if(!filtro) return -1
const { id, email } = filtro
if(id) {
return usuarios.findIndex(u => u.id == id)
} else if(email) {
return usuarios.findIndex(u => u.email == email)
}
return -1
}
module.exports = {
alterarUsuario(_, { filtro, dados }) {
const i = indiceUsuario(filtro)
if(i < 0) return null
const usuario = {
...usuarios[i],
...dados
}
usuarios.splice(i, 1, usuario)
return usuario
}
}
Neste momento temos o projeto composto da seguinte maneira:
- mutations/
- data/
- db.js
- node_modules/
- resolvers/
- index.js
- Mutation.js
- Query.js
- Usuario.js
- schema/
- index.graphql
- Mutation.graphql
- Perfil.graphql
- Query.graphql
- Usuario.graphql
- validations/
- email_validator.js
- index.js
- package-lock.json
- package.json
Onde o resolvers/Mutation.js
:
const { usuarios, proximoId } = require('../data/db')
const { emailExistente, validarEmail } = require('../validations/email_validator')
function indiceUsuario(filtro) {
if(!filtro) return -1
const { id, email } = filtro
if(id) {
return usuarios.findIndex(u => u.id == id)
} else if(email) {
return usuarios.findIndex(u => u.email == email)
}
return -1
}
module.exports = {
novoUsuario(_, { dados }) {
if (emailExistente(usuarios, dados.email) || validarEmail(dados.email)) {
throw new Error('E-mail já cadastrado ou está incorreto!')
}
const novo = {
id: proximoId(),
...dados,
perfil_id: 1,
status: 'ATIVO'
}
usuarios.push(novo)
return novo
},
excluirUsuario(_, { filtro }) {
const i = indiceUsuario(filtro)
if(i < 0) return null
const excluidos = usuarios.splice(i, 1)
return excluidos ? excluidos[0] : null
},
alterarUsuario(_, { filtro, dados }) {
const i = indiceUsuario(filtro)
if(i < 0) return null
const usuario = {
...usuarios[i],
...dados
}
usuarios.splice(i, 1, usuario)
return usuario
}
}
Começaremos segregando os resolvers de mutations por tipo de objeto, neste caso temos apenas usuarios
, deixando a aplicação com seguinte estrutura:
- mutations/
- data/
- db.js
- node_modules/
- resolvers/
- Mutation/
- index.js
- usuario.js
- index.js
- Query.js
- Usuario.js
- schema/
- index.graphql
- Mutation.graphql
- Perfil.graphql
- Query.graphql
- Usuario.graphql
- validations/
- email_validator.js
- index.js
- package-lock.json
- package.json
Onde recortaremos o conteúdo do arquivo resolvers/Mutation.js
para um novo arquivo resolvers/Mutation/usuario.js
.
// resolvers/Mutation/usuario.js
const { usuarios, proximoId } = require('../../data/db')
const { emailExistente, validarEmail } = require('../validations/email_validator')
function indiceUsuario(filtro) {
if(!filtro) return -1
const { id, email } = filtro
if(id) {
return usuarios.findIndex(u => u.id == id)
} else if(email) {
return usuarios.findIndex(u => u.email == email)
}
return -1
}
module.exports = {
novoUsuario(_, { dados }) {
if (emailExistente(usuarios, dados.email) || validarEmail(dados.email)) {
throw new Error('E-mail já cadastrado ou está incorreto!')
}
const novo = {
id: proximoId(),
...dados,
perfil_id: 1,
status: 'ATIVO'
}
usuarios.push(novo)
return novo
},
excluirUsuario(_, { filtro }) {
const i = indiceUsuario(filtro)
if(i < 0) return null
const excluidos = usuarios.splice(i, 1)
return excluidos ? excluidos[0] : null
},
alterarUsuario(_, { filtro, dados }) {
const i = indiceUsuario(filtro)
if(i < 0) return null
const usuario = {
...usuarios[i],
...dados
}
usuarios.splice(i, 1, usuario)
return usuario
}
}
E adicionaremos um arquivo resolvers/Mutation/index.js
para indexar todos os resolvers.
// resolvers/Mutation/index.js
const usuario = require('./usuario')
module.exports = {
...usuario
}
Implementar create, update e delete para Perfil
// schema/Perfil.graphql
type Perfil {
id: ID
nome: String!
}
input PerfilInput{
nome: String
}
input PerfilFiltro{
id: ID
nome: String
}
// schema/Mutation.graphql
type Mutation {
novoPerfil(
dados: PerfilInput!
):Perfil!
excluirPerfil(
filtro: PerfilFiltro!
):Perfil
alterarPerfil(
filtro: PerfilFiltro!
dados: PerfilInput!
):Perfil
}
// resolvers/Mutation/perfil.js
const { perfis, proximoId } = require('../../data/db')
const { nomeExistente } = require('../validations/name_validator')
function indicePerfil(filtro) {
if(!filtro) return -1
const { id, nome } = filtro
if(id) {
return perfis.findIndex(p => p.id == id)
} else if(nome) {
return perfis.findIndex(p => p.nome == nome)
}
return -1
}
module.exports = {
novoPerfil(_, { dados }) {
if(nomeExistente(perfis, dados.nome)) {
throw new Error('Nome de Perfil já cadastrado')
}
const novoPerfil = {
id: proximoId(),
...dados
}
perfis.push(novoPerfil)
return novoPerfil
},
excluirPerfil(_, { filtro }){
const i = indicePerfil(filtro)
if(i < 0) return null
const excluidos = perfis.splice(i, 1)
return excluidos ? excluidos[0] : null
},
alterarPerfil(_, { dados, filtro }){
if(nomeExistente(perfis, dados.nome)) {
throw new Error('Nome de Perfil já cadastrado')
}
const i = indicePerfil(filtro)
if(i < 0) return null
const perfil = {
...perfis[i],
...dados
}
perfis.splice(i, 1, perfil)
return perfil
}
}
// resolvers/Mutation/index.js
const usuario = require('./usuario')
const perfil = require('./perfil')
module.exports = {
...usuario,
...perfil
}
// validations/name_validator.js
function nomeExistente(perfis, nome) {
return perfis.some(p => p.nome === nome)
}
module.exports = {nomeExistente }
// data/db.js
let id = 1
function proximoId() {
return id++
}
const usuarios = [
{
id: proximoId(),
nome: 'Italo',
idade: 36,
email: '[email protected]',
perfil_id: 1,
status: 'ATIVO',
vip: true,
salario: 10000
},
{
id: proximoId(),
nome: 'Francisco',
idade: 0,
email: '[email protected]',
perfil_id: 2,
status: 'INATIVO',
vip: true,
salario: 0
},
{
id: proximoId(),
nome: 'Caetano',
idade: 0,
email: '[email protected]',
perfil_id: 2,
status: 'BLOQUEADO',
vip: true,
salario: 0
}
]
const perfis = [
{ id: 1, nome: 'Administrador' },
{ id: 2, nome: 'Comum' }
]
module.exports = { usuarios, perfis, proximoId }
Neste tópico implementaremos o banco de dados PostgreSQL
em nossa API com a ajuda do construtor de queries Knex.js
, que facilita a interação com bancos de dados relacionais utilizando JavaScript. Ele suporta vários sistemas de gerenciamento de bancos de dados, como PostgreSQL, MySQL, SQLite3, e outros.
Ao invés de escrever consultas SQL diretamente em strings, o Knex permite que você construa consultas usando métodos JavaScript encadeados. Isso torna o código mais legível e ajuda a evitar problemas comuns, como injeção de SQL.
Em um diretório diferente do projeto do GraphQL, criaremos um diretório chamado postgres
com um único arquivo:
# postgres/docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15.3-alpine
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- '5432:5432'
volumes:
- db:/var/lib/postgresql/data
volumes:
db:
driver: local
Use o comando para iniciar o container:
docker-compose up --build
Crei banco de dados exercicios
pelo console com os seguintes comandos:
# acessa o bash do container
docker exec -it postgres_db_1 bash
# acessa o console do postgres
psql -U postgres
# cria o banco de dados
CREATE DATABASE exercicios;
# sai do console do postgres
\q
# sai do terminal do container
exit
E o comando para entrar no console do banco de dados criado:
docker exec -it postgres_db_1 psql -U postgres -d exercicios
Recomendo o uso de uma aplicação de administração de banco de dados, como o DBeaver e conectar o novo banco de dados.
Para ele, criaremos um novo diretório banco-de-dados
e no terminal rodamos os comandos:
# entra no diretório banco-de-dados
cd banco-de-dados
# inicializa um novo projeto Node.js e criar um arquivo package.json
npm init -y
# instala as dependências knex e postgres e as persiste no package.json
npm i -s knex pg
# inicializa um projeto com o Knex.js, que é um construtor de consultas SQL para Node.js. Esse comando cria um arquivo chamado knexfile.js no diretório atual
npx knex init
Atualmente o projeto está disposto desta maneira:
- banco-de-dados
- node_modules/
- knexfile.js
- package-lock.json
- package.json
Onde:
// knexfile.js
/**
* @type { Object.<string, import("knex").Knex.Config> }
*/
module.exports = {
client: 'postgres',
connection: {
host: 'localhost',
database: 'exercicios',
user: 'postgres',
password: 'postgres'
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: 'knex_migrations'
}
};
// package.json
{
"name": "projeto",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"knex": "^3.0.1",
"pg": "^8.11.3"
}
}
# cria a migration da tabela perfis
npx knex migrate:make tabela_perfis
# cria a migration da tabela usuario
npx knex migrate:make tabela_usuarios
# cria a migration da tabela de relacionamento usuarios_perfis
npx knex migrate:make tabela_usuarios_perfis
Agora a aplicação foi modificada e está com a seguinte estrutura:
- banco-de-dados
- migrations/
- 20231018195724_tabela_perfis.js
- 20231018195745_tabela_usuarios.js
- 20231018195752_tabela_usuarios_perfis.js
- node_modules/
- knexfile.js
- package-lock.json
- package.json
Onde:
// migrations/20231018195724_tabela_perfis.js
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.createTable('perfis', table => {
table.increments('id').primary()
table.string('nome').notNullable().unique()
table.string('rotulo').notNullable()
}).then(function() { // SEED
return knex('perfis').insert([
{ nome: 'comum', rotulo: 'Comum' },
{ nome: 'admin', rotulo: 'Administrador' },
{ nome: 'master', rotulo: 'Master' }
])
})
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTable('perfis')
};
// migrations/20231018195745_tabela_usuarios.js
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.createTable('usuarios', table => {
table.increments('id').primary()
table.string('nome').notNullable()
table.string('email').notNullable().unique()
table.string('senha', 60).notNullable()
table.boolean('ativo').notNullable().defaultTo(true)
table.timestamp('data_criacao').defaultTo(knex.fn.now())
})
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTable('usuarios')
};
// migrations/20231018195752_tabela_usuarios_perfis.js
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.createTable('usuarios_perfis', table => {
table.integer('usuario_id').unsigned()
table.integer('perfil_id').unsigned()
table.foreign('usuario_id').references('usuarios.id')
table.foreign('perfil_id').references('perfis.id')
table.primary(['usuario_id', 'perfil_id'])
})
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTable('usuarios_perfis')
};
Para efetivamente criar as tabelas configuradas nas migrations utilizaremos o comando: npx knex migrate:latest
E para uma eventual necessidade de rollback utilizamos o comando: npx knex migrate:rollback
Para começarmos a manipular o banco de dados, precisaremos primeiramente de um arquivo de configuração do knex
.
Criaremos o arquivo config/db.js
.
- banco-de-dados
- config/
- db.js
- migrations/
- 20231018195724_tabela_perfis.js
- 20231018195745_tabela_usuarios.js
- 20231018195752_tabela_usuarios_perfis.js
- node_modules/
- knexfile.js
- package-lock.json
- package.json
Onde:
// config/db.js
const config = require('../knexfile')
module.exports = require('knex')(config)
Em seguida criaremos os arquivos de teste das operações no banco de dados da aplicação.
- banco-de-dados
- config/
- db.js
- migrations/
- 20231018195724_tabela_perfis.js
- 20231018195745_tabela_usuarios.js
- 20231018195752_tabela_usuarios_perfis.js
- node_modules/
- testes/
- async_await.js
- delete.js
- insert.js
- select.js
- update.js
- knexfile.js
- package-lock.json
- package.json
// testes/insert.js
// SQL: insert into perfis (nome, rotulo) values ('root345', 'Super Usuário');
const db = require('../config/db')
// Primeira opção
const novoPerfil = { nome: 'cadastrador', rotulo: 'Cadastrador' }
db('perfis').insert(novoPerfil)
.then(res => console.log(res))
.catch(err => console.log(err.detail))
.finally(() => db.destroy())
// Segunda opção
const perfilSU = { nome: 'root' + Math.random(), rotulo: 'Super Usuário' }
db.insert(perfilSU).into('perfis')
.then(res => console.log(res))
.catch(err => console.log(err.detail))
.finally(() => db.destroy())
// testes/select.js
// SQL: select * from perfis;
// SQL: select nome, id from perfis;
const db = require('../config/db')
// Primeira opção não selecionando atributos
db('perfis')
.then(res => console.log(res))
.finally(() => db.destroy())
// Primeira opção selecionando atributos
db('perfis').select('nome', 'id')
.then(res => console.log(res))
.finally(() => db.destroy())
// Segunda opção
db.select('nome', 'id')
.from('perfis')
.limit(2)
.offset(2)
.then(res => console.log(res))
.finally(() => db.destroy())
// Terceira opção filtrando com where
db('perfis')
// .where('id', '=', 2)
// .where('nome', 'ilike', 'ad%')
// .whereNot({ id: 2 })
// .whereIn('id', [1, 2, 3])
.where({ id: 2 })
.then(res => console.log(res))
.finally(() => db.destroy())
// testes/update.js
// SQL: update perfis set nome = 'funcionario', rotulo = 'Funcionário' where id = 2;
const db = require('../config/db')
const novosDados = {
nome: 'funcionario',
rotulo: 'Funcionario'
}
db('perfis')
.where({ id: 2 })
.update(novosDados)
.then(res => console.log(res))
.finally(() => db.destroy())
// testes/delete.js
// SQL: delete from perfis where id = 1;
const db = require('../config/db')
db('perfis')
.where({ id: 1 })
.delete()
.then(res => console.log(res))
.finally(() => db.destroy())
// testes/async_await.js
const db = require('../config/db')
const novoUsuario = {
nome: 'Caetano',
email: '[email protected]',
senha: '123456'
}
async function exercicio() {
// count
const { qtde } = await db('usuarios').count('* as qtde').first()
// insert
if(qtde === 0) {
await db('usuarios').insert(novoUsuario)
}
// select
let { id } = await db('usuarios').select('id').limit(1).first()
// update
await db('usuarios').where({ id }).update({
nome: 'Francisco', email: '[email protected]'
})
return db('usuarios').where({ id })
}
exercicio()
.then(usuario => console.log(usuario))
.finally(() => db.destroy())
Crie um usuário, dois perfis e atribua os perfis ao usuário validando a existência do objeto no momento da criação. Caso exista, deve ser atualizado no banco de dados.
const db = require('../config/db')
async function salvarUsuario(nome, email, senha) {
let [ usuario ] = await db('usuarios').where({ email })
if(!usuario) {
[ usuario ] = await db('usuarios')
.insert({ nome, email, senha })
.returning('*')
} else {
[ usuario ] = await db('usuarios')
.where({ id: usuario.id})
.update({ nome, email, senha })
.returning('*')
}
return usuario
}
async function salvarPerfil(nome, rotulo) {
let [ perfil ] = await db('perfis').where({ nome })
if(!perfil) {
[ perfil ] = await db('perfis')
.insert({ nome, rotulo })
.returning('*')
} else {
[ perfil ] = await db('perfis')
.where({ id: perfil.id })
.update({ nome, rotulo })
.returning('*')
}
return perfil
}
async function adicionarPerfis(usuario, ...perfis) {
await db('usuarios_perfis')
.where({ usuario_id: usuario.id })
.delete()
for (let perfil of perfis) {
let usuarios_perfis = await db('usuarios_perfis')
.insert({
usuario_id: usuario.id,
perfil_id: perfil.id
}).returning('*')
console.log(usuarios_perfis)
}
}
async function executar() {
const usuario = await salvarUsuario('Caetano', '[email protected]', '123456')
const perfilA = await salvarPerfil('rh', 'Recursos Humanos')
const perfilB = await salvarPerfil('fin', 'Financeiro')
console.log(usuario)
console.log(perfilA)
console.log(perfilB)
await adicionarPerfis(usuario, perfilA, perfilB)
}
executar()
.catch(err => console.log(err))
.finally(() => db.destroy())
O tratamento de cache personalizado em aplicações que utilizam GraphQL pode ser uma parte crucial para melhorar a eficiência e a performance do sistema. Como o GraphQL permite que os clientes solicitem exatamente os dados que precisam, isso implica que os clientes podem receber atualizações parciais ou incrementais nos dados, em vez de obter um conjunto completo a cada requisição. Isso cria oportunidades para estratégias de cache mais sofisticadas.
Aqui estão algumas práticas comuns para o tratamento de cache personalizado com GraphQL:
- Usar Diretivas de Cache:
- O GraphQL permite a definição de diretivas personalizadas que podem ser usadas para sinalizar o comportamento de cache desejado em nível de campo ou consulta. Por exemplo, uma diretiva @cache pode ser usada para indicar que um campo pode ser armazenado em cache.
- Utilizar Estratégias de Cache Baseadas em Identificadores Únicos (IDs):
- Muitas vezes, é útil associar identificadores únicos (como IDs de banco de dados) a objetos no GraphQL. Esses IDs podem ser usados para identificar e buscar objetos armazenados em cache.
- Cache por Campo ou Fragmento:
- Em vez de armazenar em cache a resposta inteira de uma consulta, é possível armazenar em cache campos individuais ou fragmentos de dados. Isso pode ser particularmente útil em consultas complexas onde apenas partes dos dados mudam entre requisições.
- "Invalidação de Cache em Mutações**:
- Quando ocorrem mutações que alteram os dados no servidor, é importante invalidar o cache relevante para garantir que os dados atualizados sejam recuperados na próxima consulta.
- Considerar a Duração do Cache:
- É importante definir o tempo de vida (TTL) dos dados armazenados em cache para garantir que as informações estejam sempre atualizadas o suficiente para atender às necessidades do aplicativo.
- Utilizar Bibliotecas e Ferramentas de Cache:
- Existem bibliotecas e ferramentas que podem facilitar o tratamento de cache personalizado em aplicações GraphQL. Por exemplo, ferramentas como Apollo Client oferecem recursos de cache avançados.
- Monitorar e Otimizar o Desempenho do Cache:
- É importante monitorar o desempenho do cache para garantir que ele está sendo eficaz. Isso pode envolver a análise de métricas como taxas de acerto no cache, tempo de resposta e uso de recursos do servidor.