Uma linguagem de consultas (QL - Query Language) para grafos onde consultamos os nós (Node, comumente referidos como objetos) e também conexões entre eles (Connection Edges). Diferentemente de outras abordagens, como REST, temos apenas um único endpoint a qual enviamos as consultas (query) ou mutações (mutation - escritas seguidas de leituras).
A linguagem GraphQL foi criada pelo Facebook baseada em problemas e soluções desenvolvidas para melhorar a obtenção de dados e foi publicamente anunciada na React.js Conf 2015. Ou seja, um grande usuário de REST, conhecedor do padrão, suas limitações e como melhorará-lo. Após o anúncio em 2015 algumas empresas já oferecem ou migraram sua API para GraphQL, incluindo Shopify e GitHub o qual fez um post bem detalhado explicando o motivo da conversão.
Um dos principais problemas encontrados com REST e resolvidos pelo GraphQL é fornecer exatamente o que o cliente precisa. Nada a mais e nada a menos. Isso inclui navegar relacionamentos com outros nós (objetos) em uma única consulta, otimizando imensamente o tráfego de informações ao cliente e evitando que novos endpoints específicos sejam criados para otimizar um cliente em particular.
Nas consultas GraphQL navega-se os tipos complexos definidos pela aplicação até chegar aos tipos simples (scalars), os quais serão retornados em estrutura idêntica à requisitada. Apenas os campos requisitados serão retornados e na ordem em que foram requisitados, permitindo assim fácil interpretação e validação do retorno (em geral JSON), bem como evita-se que novas versões da API e consequentemente novos endpoints sejam criados para adicionar um novo campo ou satisfazer um cliente em específico -- novos campos, consultas, mutações e até parâmetros opcionais não necessitam de nova versão da API afinal os clientes anteriores continuarão funcionando perfeitamente.
query { bin(number: "509069") { issuer { name } } }
Retorno (geralmente JSON):
{ "bin": { "issuer": { "name": "Banco do Brasil" } } }
Esta simples consulta esconde uma complexidade: a consulta por BIN retorna um campo complexo: CardIssuer, o qual tem um campo name. Isso seria mapeado em outro endpoint, ou seja, estamos fazendo 2 consultas em uma. Em REST teríamos:
>>> HTTP.GET('/bin/509069') { "issuer": {"href": "/cardIssuer/DEA90DD0-E1C3-4EC6-B581-8EC3AC36A7E4"}, "product": {"href": "/cardProduct/5C0CA4F4-B720-4AEA-A4C1-9C39A04C1E83"}, "country": "BRA", "isInternational": true, "isCompany": false... } >>> HTTP.GET('/cardIssuer/DEA90DD0-E1C3-4EC6-B581-8EC3AC36A7E4') {"name": "Banco do Brasil", "legalName": "Banco do Brasil S.A", ...}
Outro exemplo: consultar todos os cartões de um portador para listar os 4 últimos dígitos (last4). As consultas REST seriam: 1) ao portador /cardHolder/$cardHolderId e 2) ao cartão /card/$cardId:
REST:
>>> HTTP.GET('/cardHolder/$cardHolderId') { "cards": [ "c1": {"href": "/card/c1"}, "c2": {"href": "/card/c2"}, "c3": {"href": "/card/c3"} ], "name": "João Silva"... } >>> HTTP.GET('/card/c1') {"last4": "1234", "expiry": {"month": 1, "year": 2018}, "status": "ACTIVE"...} >>> HTTP.GET('/card/c2') {"last4": "4444", "expiry": {"month": 2, "year": 2020}, "status": "SUSPENDED"...} >>> HTTP.GET('/card/c3') {"last4": "5555", "expiry": {"month": 3, "year": 2022}, "status": "ACTIVE"...}
GraphQL:
query TodosOsUltimos4DoPortador($cardHolderId: ID!) { node(id: $cardHolderId) { ... on CardHolder { cards { edges { node { last4 } } } } } }
Resultado em JSON:
{ "node": { "cards": { "edges": [ { "node": { "last4": "1234" } }, { "node": { "last4": "4444" } }, { "node": { "last4": "5555" } } ] } } }
Em REST existem variações que permitem definir quais campos retornar, por exemplo /card/c3?fields=last4 para retornar apenas last4. Em outros casos pode-se pedir inclusive outros subcampos, reduzindo o número de chamadas HTTP. A API do próprio Facebook é assim e foi considerada para a criação do GraphQL.
No entanto, para otimizar a obtenção de dados de um cliente específico terminamos criando um endpoint REST especial para atendê-lo. Caso o cliente mude sua necessidade, precisaríamos criar um novo endpoint REST e uma nova versão da API! Seria muito mais interessante que cada cliente pudesse especificar o que quer naquele momento sem necessitar de um novo suporte do servidor. E é exatamente isso que o GraphQL oferece!
GraphQL utiliza tipagem forte, ou seja, não existe conversão automática ou implícita de tipos. Toda a informação de tipos é previamente conhecida via Schema e pode ser requisitada em tempo de execução via consultas de introspeção, tornando possível o uso de ferramentas como o GraphiQL um editor e executor de GraphQL feito diretamente no navegador de internet. A introspeção inclui a documentação dos tipos, consultas e mutações em formato Markdown.
Com toda a estrutura definida num único Schema, explicitando todos os tipos, consultas e mutações, não é necessário um sistema auxiliar como Swagger ou WSDL.
Para mais informações:
Usando GraphQL em:
Outros usuários de GraphQL (APIs Públicas):
Outros usuários ainda sem API pública: http://graphql.org/users/
Leitura Complementar:
As consultas GraphQL têm início no tipo Query e são especificadas dentro de um bloco query { CONSULTA }. No sistema Elo temos diversas consultas, como por exemplo obter informações de um cartão à partir do seu BIN.
Para consultar informações de um cartão à partir do seu BIN utilizamos a consulta inicial bin(number: String) a qual retorna um tipo BIN, o qual tem um campo issuer (emissor) do tipo CardIssuer, o qual tem um campo name do tipo String.
Exemplo de consulta nome do emissor dado um BIN:
query { bin(number: "509069") { issuer { name } # Deverá retornar Banco do Brasil } }
Consultando o Schema ou a documentação, percebe-se as declarações:
type Query { bin(number: String!): BIN } type BIN { ... issuer: CardIssuer! product: CardProduct! ... } type CardIssuer { ... name: String! ... } type CardProduct { ... name: String! ... }
Retorno:
{ "bin": { "issuer": { "name": "Banco do Brasil" } } }
Ou seja, a consulta que enviamos basicamente navega o grafo em busca dos tipos escalares, no caso a String que define o nome do emissor. Em uma única consulta obtemos tudo o que queremos e apenas o que queremos, na ordem em que queremos. Veja que não são retornados outros campos.
Isso permite que novos campos sejam introduzidos sem que uma nova versão da API seja criada, não é necessário utilizar um novo endpoint como usualmente acontece com REST, afinal os novos campos não serão utilizados pelos aplicativos já existentes e nenhuma modificação será necessária. As versões de API somente serão necessárias caso um novo parâmetro obrigatório (non-null) seja introduzido em uma consulta ou mutação; ou ao remover consultas ou mutações.
Detalhes GraphQL:
- Texto (String) são codificados em UTF-8 entre aspas duplas (
");- Exclamação (
!) após o tipo diz que este campo nunca seránull;- Cerquilha (
#) inicia um comentário.
Caso múltiplas consultas sejam necessárias numa mesma requisição, basta colocá-las dentro do mesmo bloco. Elas serão retornadas em ordem:
query { bin(number: "509069") { issuer { name } } merchant(legalIds: {cnpj: "33014556000196"}) { name } }
Resultado:
{ "bin": { "issuer": { "name": "Banco do Brasil" } }, "merchant": { "name": "Lojas Americanas S.A." } }
Como vimos nos exemplos anteriores, o retorno terá como chave o nome da consulta sem os parâmetros. Caso precise de várias consultas de mesmo nome com parâmetros diferentes, utilize apelidos (alias), prefixando a consulta com Apelido:, exemplo:
query { consulta1: bin(number: "509068") { product { name } } consulta2: bin(number: "509069") { product { name } } }
Retorno:
{ "consulta1": { "product": { "name": "Nanquim" } }, "consulta2": { "product": { "name": "Grafite" } } }
Acima utilizamos uma consulta com parâmetro obrigatório: bin(number: String!), note que number é uma String não nula (!) e caso o parâmetro não seja especificado, null seria utilizado.
No entanto a maioria das consultas não tem parâmetros. Por exemplo issuer { name } são duas consultas: issuer: CardIssuer e então name: String, ambas não recebem parâmetro algum.
Outras consultas tem parâmetros opcionais, como é o caso de binTable que pode receber ifModifiedSince: String e ifNoneMatch: String para evitar baixar novamente uma tabela já conhecida. Neste caso, na primeira chamada a binTable não temos os valores, logo podemos deixá-los de fora da consulta:
query { binTable { etag bins { number } }
Resultado:
{ "binTable": { "etag": "12345", "bins": [ {"number": "509068"}, {"number": "509069"} ] } }
Agora temos etag, logo podemos utilizá-la em binTable(ifNoneMatch: "12345") de modo a evitar que a tabela seja retornada caso o etag seja o mesmo... neste caso retornará null:
query { binTable(ifNoneMatch: "12345") { etag bins { number } }
Resultado:
{ "binTable": { "etag": "12345", "bins": null } }
Em GraphQL podemos isolar campos de uma consulta em uma unidade comum chamada fragment (fragment). Fragmentos sempre definem um tipo para operarem e tem basicamente dois usos:
Para o primeiro caso, reuso, utilizamos fragmentos com nome. Relembrando a consulta que utiliza apelidos vemos que ela replica a consulta bins { number }. Neste exemplo artificial utilizamos apenas um campo, logo não aparenta muito problema replicá-lo. Porém caso precisássemos de todos os campos, seria bem tedioso e fácil de errar. Para melhorar o exemplo utilizaremos fragmentos:
fragment CamposBin on BIN { product { name } } query { consulta1: bin(number: "509068") { ...CamposBin } consulta2: bin(number: "509069") { ...CamposBin } }
No entanto algumas consultas retornam uniões ou interfaces, nestes casos precisamos utilizar fragmentos para definirmos sobre qual tipo estamos consultando os campos. Podemos utilizar fragmentos com nomes, porém para evitar o inconveniente de declararmos e então utilizarmos, seguimos com a opção inline fragment:
query { node(id: "xpto") { ... on CardHolder { name birthday } } }
Neste caso a consulta node(id: String!) retorna nós que implementam a interface Node, como CardHolder (portador), CardIssuer (emissor), Merchant (comerciante), Wallet (carteira)... Estamos consultando o nó com identificador global único "xpto" e caso ele seja um CardHolder, queremos o nome e a data de aniversário. Caso ele seja de outro tipo, nenhum valor será retornado.
Detalhe: A interface
Node { id }e a consultanode(id)foram inicialmente definidas pelo Facebook Relay em Object Identification porém são utilizadas por diversos softwares em parceria com fragmentos. Veja mais emGraphQLRelay.
Ao passo que a consulta GraphQL é descrita em texto e é fácil de manipular deve-se evitar gerar uma consulta e incluir dados recebidos, sendo sujeito a erros e injeção de códigos que podem levar a vazamento de informações, como aconteceu inúmeras vezes com SQL. Ao invés disso recomenda-se utilizar variáveis, as quais são definidas como $NOME. Por exemplo para fazermos uma versão parametrizada da consulta anterior, declaremos a variável $cardHolderId:
query cardHolderInfo($cardHolderId: ID!) { node(id: $cardHolderId) { ... on CardHolder { name birthday } } }
As mutações GraphQL se referem a escritas (atualizações) seguidas de uma leitura, têm início no tipo Mutation e são especificadas dentro de um bloco mutation { MUTAÇÃO }. No sistema Elo temos diversas mutações, como por exemplo criar um token para um cartão (CardToken) ou cadastrar uma transação de um facilitador (PSP).
NOTA: As mutações retornam um tipo de consulta, logo é necessário conhecer
Consultas - _Query_antes de continuar a leitura.
Por ser mais simples, utilizaremos a API de facilitadores (PSP) como referência: createPSPTransaction(input: CreatePSPTransactionInput!): CreatePSPTransactionPayload.
Em nossa API utilizamos chamadas idempotentes, ou seja, podem ser executadas mais de uma vez sem alterar o resultado final. Para isso todos os parâmetros input terão um campo clientMutationId: String o qual será utilizado pelo servidor para evitar duplicatas. Os usuários que reenviarem as mutações, por exemplo devido a problemas com conexão, devem reenviar o mesmo clientMutationId e o servidor irá verificar se tal mutação já foi executada, neste caso não será re-executada e apenas a consulta será feita. Este campo também é retornado na consulta.
mutation { createPSPTransaction(input: { clientMutationId: "ADD66061", # mude para cada mutação! bin: "509069", # Banco do Brasil Grafite last4: "1234", auth: "abc1234", # Fornecido pelo emissor durante a transação timestamp: "2017-01-20T12:34:56Z", # Data e Horário da transação value: 123.45, # Valor em `R$` usn: "11112222", # Número sequencial único mcc: 2244, # Código da Categoria (ISO18245)[https://en.wikipedia.org/wiki/ISO_18245] legalId: "33014556000196", # Lojas Americanas S.A. pos: "111222333", # Código do Ponto de vendas, opcional. status: APPROVED, # Transação aprovada }) { clientMutationId pspId legalId }
Retorno:
{ "createPSPTransaction": { "clientMutationId": "ADD66061", "pspId": 1234, "legalId": "33014556000196" } }
Detalhe: Esta assinatura, composta de um único parâmetro
inputcom tipo de entrada terminado em<NomeDaMutação>Input, retornando o tipo de consulta<NomeDaMutação>Payloadfoi inicialmente definida pelo Facebook Relay em Mutations porém são utilizadas por diversos softwares em parceria com fragmentos. Veja mais emGraphQLRelay.
Detalhe GraphQL: ao passo que as consultas são estritamente leituras e não tem efeitos colaterais e portanto são executas em paralelo, as escritas são executadas sequencialmente na ordem em que foram enviadas.
Da mesma forma que descrito em Múltiplas Consultas, utilize apelidos:
mutation { t0: createPSPTransaction(input: { clientMutationId: "ADD66061", # mude para cada mutação! bin: "509069", # Banco do Brasil Grafite last4: "1234", auth: "abc1234", # Fornecido pelo emissor durante a transação timestamp: "2017-01-20T12:34:56Z", # Data e Horário da transação value: 123.45, # Valor em `R$` usn: "11112222", # Número sequencial único mcc: 2244, # Código da Categoria (ISO18245)[https://en.wikipedia.org/wiki/ISO_18245] legalId: "33014556000196", # Lojas Americanas S.A. pos: "111222333", # Código do Ponto de vendas, opcional. status: APPROVED, # Transação aprovada }) { clientMutationId pspId legalId } t1: createPSPTransaction(input: { clientMutationId: "61186EF5382", # mude para cada mutação! bin: "509069", # Banco do Brasil Grafite last4: "1234", auth: "xpto4455", # Fornecido pelo emissor durante a transação timestamp: "2017-02-21T10:11:12Z", # Data e Horário da transação value: 5.67, # Valor em `R$` usn: "1234567890", # Número sequencial único mcc: 2244, # Código da Categoria (ISO18245)[https://en.wikipedia.org/wiki/ISO_18245] legalId: "00776574000660", # B2W # `pos: "123",` # Código do Ponto de vendas, opcional. status: RETURNED, # Transação desfeita }) { clientMutationId pspId legalId } }
Retorno:
{ "t0": { "clientMutationId": "ADD66061", "pspId": 1234, "legalId": "33014556000196" }, "t1": { "clientMutationId": "61186EF5382", "pspId": 1234, "legalId": "00776574000660" } }
Logo que o Facebook anunciou o GraphQL também forneceu integração com seu sistema de interface com o usuário React, esta integração sendo denominada Relay.
Com o objetivo de facilitar a integração foram definidas várias convenções as quais seguimos em nossa API. Estas convenções não impossibilitam uso de outras ferramentas, pelo contrário, várias delas já utilizam as mesmas convenções e fornecem facilitadores nestes casos.
Object Identification (Especificação) define a interface Node com um único campo id e a consulta node(id): Node.
É utilizada em conjunto com fragmentos para declaração de componentes visuais (React.Component), interagindo com cache. Exemplo:
query { node(id: "xpto") { ... on CardHolder { name birthday } } }
Mutations define o uso de clientMutationId para idempotência e assinatura padrão das mutações: um único parâmetro de entrada input com nome terminado em Input e contendo clientMutationId; O tipo de retorno terminado em Payload contendo também clientMutationId.
Connection (Especificação) define um padrão para listagens paginadas. As conexões entre nós retornam informações de paginação (pageInfo) e também as arestas (edges), cada qual com o vértice (node) e o cursor opaco para paginação relativa.
As consultas têm parâmetros o cursor de início (after: String) ou fim (before: String), o número de conexões a retornar (first e last), tipo de retorno terminado em Connection contendo os campos edges (arestas) e informação de paginação (pageInfo).
Em pageInfo temos informações como se existe próxima página (hasNextPage), se existe página anterior (hasPreviousPage), o primeiro cursor (startCursor, para ir para a página 1), o último cursor (endCursor para ir para a última página).
Na nossa API também oferecemos em <Tipo>Connection o campo totalCount com o número total de conexões (não só os retornados nesta consulta).
Em cada aresta <Tipo>Edge retorna-se o vértice (node) e o cursor opaco para paginação até o mesmo (cursor). Em alguns casos pode-se carregar outros atributos, como a distância em Kilômetros até um estabelecimento ou caixa eletrônico.
Os vértices retornados devem implementar Node e consequentemente interagem com Object Identification citado anteriormente, uma listagem que retorna novas informações para um node(id) já conhecido pode notificar os componentes para mostrar os novos valores.
Existem bibliotecas em diversas linguagens para facilitar o uso, porém devido ao transporte HTTP + JSON e as consultas descritas em texto como vimos em Introdução e Consultas - _Query_, é extremamente fácil criar um cliente em qualquer linguagem e ferramenta que suporte o protocolo HTTP e a serialização JSON. Abaixo podemos ver exemplos com a ferramenta de linha de comando curl. Veja detalhes em http://graphql.org/learn/serving-over-http/
Consulta GraphQL por identificador global único de todos os emissores de cartão:
query { cardIssuers { edges { node { id } } } }
$ curl --request POST \ --header 'Content-Type: application/json' \ --header 'Access-Token: TOKEN-OBTIDO-VIA-OAUTH' \ --data '{"query": "query { cardIssuers { edges { node { id } } } }"}' \ http://endpoint/graphql { "data": { "cardIssuers": { "edges": [ {"node": {"id": "DD07219D-135F-4EB2-AF0E-93AE50EDFD6E"}}, {"node": {"id": "67869651-B122-4994-B789-1828F281F572"}}, {"node": {"id": "30B44596-C88B-41F2-BB1A-4BE7BCCE7CF6"}} ] } } }
Ou seja, enviamos uma requisição POST com o cabeçalho Content-Type: application/json então um corpo com o conteúdo JSON que deve conter os membros:
query: com o texto da nossa consulta GraphQL. Obrigatório;operationName: caso mais de uma consulta seja fornecida, elas devem conter um nome e aqui especificamos qual a consulta que desejamos executar. Opcional;variables: um objeto JSON com as variáveis a substituir na consulta. Por exemplo, se usássemos $cardHolderId: String, deveríamos passar: "variables": {"cardHolderId": "xpto"}. Opcional.A resposta também é em formato JSON e contém basicamente dois membros:
data: objeto JSON com o resultado da consulta, em caso de sucesso. A estrutura retornada será a mesma da consulta até chegar em tipos escalares. Datas e horários (Date e DateTime) são representados em texto utilizando ISO 8601.errors: vetor com objetos JSON contendo os erros, cada entrada do vetor terá o membro message com a mensagem. Não está presente em caso de sucesso.Para a identificação do usuário e controle de acesso o cabeçalho Access-Token é utilizado. Nele deve constar o token de acesso retornado, no exemplo acima seria o texto TOKEN-OBTIDO-VIA-OAUTH.