Preâmbulo
Antes de tudo, este artigo foi desenvolvido para os leitores que já estão familiarizados com o GraphQL e mais sobre os meandros e as nuances de como trabalhar com ele. No entanto, espero que seja útil para iniciantes.
O GraphQL é uma ótima ferramenta. Eu acho que muitas pessoas já conhecem e entendem suas vantagens. No entanto, há algumas nuances a serem observadas ao criar suas APIs baseadas em GraphQL.
Por exemplo, o GraphQL permite retornar ao consumidor (usuário ou programa) solicitando os dados apenas na parte da qual esse consumidor está interessado. No entanto, ao construir um servidor, é muito fácil cometer um erro, o que leva ao fato de que dentro do servidor (que pode ser, entre outras coisas, distribuído), os dados serão executados em "pacotes" completos. Isso se deve principalmente ao fato de o GraphQL pronto para uso não fornecer ferramentas convenientes para analisar uma consulta recebida, e as interfaces nele estabelecidas não estão bem documentadas.
Fonte do problema
Vejamos um exemplo típico de uma implementação não ideal (abra a imagem em uma janela separada, se for mal lida):

Suponha que nosso consumidor seja um determinado aplicativo ou componente da "lista telefônica" que solicite a partir de nossa API apenas o identificador, nome e número de telefone dos usuários armazenados por nós. Ao mesmo tempo, nossa API é muito mais abrangente, permitindo acesso a outros dados, como o endereço físico da residência e o endereço de e-mail dos usuários.
No momento da troca de dados entre o consumidor e a API, o GraphQL realiza perfeitamente todo o trabalho que precisamos - somente os dados solicitados serão enviados em resposta à solicitação. O problema neste caso está no ponto de amostragem de dados do banco de dados - ou seja, na implementação interna de nosso servidor, e consiste no fato de que, para cada solicitação recebida, selecionamos todos os dados do usuário no banco de dados, apesar de não precisarmos de alguns deles. Isso gera carga excessiva no banco de dados e leva à circulação de tráfego excessivo dentro do sistema. Com um número significativo de consultas, você pode obter uma otimização significativa alterando a abordagem da amostragem de dados e selecionando apenas os campos solicitados. Ao mesmo tempo, não importa qual seja a nossa fonte de dados - um banco de dados relacional, tecnologia NoSQL ou outro serviço (interno ou externo). Qualquer comportamento não ideal pode ser afetado por qualquer implementação. Neste caso, o MySQL é selecionado simplesmente como exemplo.
Solução
É possível otimizar esse comportamento do servidor se analisarmos os argumentos que chegam à função resolve()
:
async resolve(source, args, context, info) {
É o último argumento, info
, que é de particular interesse para nós, neste caso. Voltamos à documentação e analisamos em detalhes em que consiste a função resolve()
e o argumento em que estamos interessados:
type GraphQLFieldResolveFn = ( source?: any, args?: {[argName: string]: any}, context?: any, info?: GraphQLResolveInfo ) => any type GraphQLResolveInfo = { fieldName: string, fieldNodes: Array<Field>, returnType: GraphQLOutputType, parentType: GraphQLCompositeType, schema: GraphQLSchema, fragments: { [fragmentName: string]: FragmentDefinition }, rootValue: any, operation: OperationDefinition, variableValues: { [variableName: string]: any }, }
Portanto, os três primeiros argumentos transmitidos ao resolvedor são a source
- os dados transmitidos do nó pai na árvore GraphQL do esquema, args
- os argumentos de solicitação (que vêm da consulta) e o context
- o objeto de contexto de execução definido pelo desenvolvedor, geralmente chamado para transmitir alguns dados globais nos "resolvedores". E, finalmente, o quarto argumento é a meta-informação sobre a solicitação.
O que podemos extrair do GraphQLResolveInfo
para resolver nosso problema?
Suas partes mais interessantes são:
fieldName
é o nome do campo atual do esquema GraphQL. I.e. corresponde ao nome do campo especificado no esquema para esse resolvedor. Se capturarmos o objeto de info
no campo de users
, como no exemplo acima, serão "usuários" que serão contidos como o valor de fieldName
fieldNodes
- coleção (matriz) de nós que foram solicitados na consulta. Apenas o que é necessário!fragments
- uma coleção de fragmentos da solicitação (caso a solicitação tenha sido fragmentada). Também informações importantes para recuperar os campos de dados finais.
Portanto, como solução, precisamos analisar a ferramenta de info
e selecionar a lista de campos que nos chegaram da consulta e depois passá-los para a consulta SQL. Infelizmente, o pacote GraphQL do Facebook "out of the box" não fornece nada para simplificar esta tarefa. No geral, como a prática demonstrou, essa tarefa não é tão trivial, dado o fato de que as solicitações podem ser fragmentadas. Além disso, essa análise tem uma solução universal, que é posteriormente simplesmente copiada de um projeto para outro.
Então, decidi escrevê-lo como uma biblioteca de código aberto sob a licença ISC . Com sua ajuda, a solução para analisar os campos de consulta recebidos é resolvida de maneira bastante simples, por exemplo, no nosso caso, da seguinte maneira:
const { fieldsList } = require('graphql-fields-list');
fieldsList(info)
, nesse caso, faz todo o trabalho para nós e retorna uma matriz "plana" de campos filho para esse resolvedor, ou seja, nossa consulta SQL final terá a seguinte aparência:
SELECT id, name, phone FROM users;
Se alterarmos a solicitação recebida para:
query UserListQuery { users { id name phone email } }
a consulta SQL se transformará em:
SELECT id, name, phone, email FROM users;
No entanto, nem sempre é possível lidar com um desafio tão simples. Freqüentemente, aplicativos reais são muito mais complexos em estrutura. Em algumas implementações, podemos precisar descrever o resolvedor no nível superior com relação aos dados no esquema final do GraphQL. Por exemplo, se decidimos usar a biblioteca Relay , gostaríamos de usar um mecanismo pronto para dividir coleções de objetos de dados em páginas, o que leva ao fato de que nosso esquema GraphQL será construído de acordo com certas regras. Por exemplo, refazemos nosso esquema dessa maneira (TypeScript):
import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; import { connectionDefinitions, connectionArgs, nodeDefinitions, fromGlobalId, globalIdField, connectionFromArray, GraphQLResolveInfo, } from 'graphql-relay'; import { fieldsList } from 'graphql-fields-list'; export const { nodeInterface, nodeField } = nodeDefinitions(async (globalId: string) => { const { type, id } = fromGlobalId(globalId); let node: any = null; if (type === 'User') { node = await database.select(`SELECT id FROM user WHERE id="${id}"`); } return node; }); const User = new GraphQLObjectType({ name: 'User', interfaces: [nodeInterface], fields: { id: globalIdField('User', (user: any) => user.id), name: { type: GraphQLString }, email: { type: GraphQLString }, phone: { type: GraphQLString }, address: { type: GraphQLString }, } }); export const { connectionType: userConnection } = connectionDefinitions({ nodeType: User }); const Query = new GraphQLObjectType({ name: 'Query', fields: { node: nodeField, users: { type: userConnection, args: { ...connectionArgs }, async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) {
Nesse caso, a connectionDefinition
from Relay adicionará edges
, node
, pageInfo
e nós de cursor
ao esquema, ou seja, agora precisaremos reconstruir nossas consultas de maneira diferente (não vamos nos concentrar na paginação agora):
query UserListQuery { users { edges { node { id name phone email } } } }
Portanto, resolve()
função implementada no nó de users
agora terá que determinar quais campos são solicitados não para si, mas para o nó do node
filho aninhado, que, como vemos, é relativo aos users
ao longo do caminho edges.node
.
fieldsList
da fieldsList
graphql-fields-list
ajudará a resolver esse problema; para isso, você deve passar a opção de path
correspondente. Por exemplo, aqui está a implementação em nosso caso:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node' }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
Também no mundo real, pode ser que no esquema GraphQL tenha definido apenas um nome de campo e no esquema do banco de dados outros nomes de campos correspondam a eles. Por exemplo, suponha que a tabela de usuários no banco de dados tenha sido definida de forma diferente:
CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fullName VARCHAR(255), email VARCHAR(255), phoneNumber VARCHAR(15), address VARCHAR(255) );
Nesse caso, os campos da consulta GraphQL devem ser renomeados antes de serem incorporados na consulta SQL. fieldsList
ajudará nisso se você passar um mapa de conversão de nome na opção de transform
correspondente:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node', transform: { phone: 'phoneNumber', name: 'fullName' }, }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
E, no entanto, às vezes, a conversão para uma matriz plana de campos não é suficiente (por exemplo, se a fonte de dados retornar uma estrutura complexa com aninhamento). Nesse caso, a função fieldsMap
da fieldsMap
graphql-fields-list
chegará ao resgate, que retorna a árvore inteira dos campos solicitados como um objeto:
const { fieldsMap } = require(`graphql-fields-list`);
Se assumirmos que o usuário é descrito por uma estrutura complexa, obteremos tudo. Esse método também pode usar o argumento de path
opcional, que permite obter um mapa apenas da ramificação necessária de toda a árvore, por exemplo:
const { fieldsMap } = require(`graphql-fields-list`);
A transformação de nomes em cartões atualmente não é suportada e permanece à mercê do desenvolvedor.
Fragmentação de Solicitação
O GraphQL suporta fragmentação de consultas, por exemplo, podemos esperar que o consumidor nos envie essa solicitação (aqui nos referimos ao esquema original, um pouco forçado, mas sintaticamente correto):
query UsersFragmentedQuery { users { id ...NamesFramgment ...ContactsFragment } } fragment NamesFragment on User { name } fragment AddressFragment on User { address } fragment ContactsFragment on User { phone email ...AddressFragment }
Nesse caso, você não deve se preocupar, e fieldsList(info)
e fieldsMap(info)
nesse caso retornarão o resultado esperado, pois eles levam em consideração a possibilidade de fragmentar solicitações. Portanto, fieldsList(info)
retornará ['id', 'name', 'phone', 'email', 'address']
e fieldsMap(info)
, respectivamente, retornará:
{ id: false, name: false, phone: false, email: false, address: false }
PS
Espero que este artigo tenha ajudado a esclarecer algumas das nuances do trabalho com o GraphQL no servidor, e a biblioteca graphql-fields-list pode ajudá-lo a criar soluções ideais no futuro.
UPD 1
A versão 1.1.0 da biblioteca foi lançada - foi adicionado suporte para as @skip
e @include
nas solicitações. Por padrão, a opção está habilitada, se necessário, desabilite-a assim:
fieldsList(info, { withDirectives: false }) fieldsMap(info, null, false);