Blog

🕸 Como e onde o GraphQL pode melhorar o WordPress, complementando a REST API

Leonardo Losoviz
Por Leonardo Losoviz ·

Atualização 01/05/2024: Confira a comparação Gato GraphQL vs WP REST API.

No último fim de semana publiquei o artigo do blog 🦸🏿‍♂️ Gato GraphQL agora é transpilado de PHP 8.0 para 7.1.

Depois de compartilhar o post no Reddit's /r/php, a comunidade iniciou uma discussão animada sobre o quanto vale a pena usar GraphQL no WordPress, o quão diferente ele é da WP REST API, e o quanto é justificado trazer mais uma API para o WordPress.

Acho que a maioria dos comentários está certeira, e outros estão deixando de fora algumas informações importantes. GraphQL não é apenas uma interface, mas também uma implementação. Isso significa que diferentes servidores GraphQL, de diferentes fornecedores, podem ter sido projetados para priorizar características distintas. Por isso, nem sempre podemos ter uma expectativa unificada do que GraphQL oferece, ou uma compreensão completa de como um motor GraphQL funciona.

Por exemplo, a experiência com GraphQL no WordPress e no Laravel será diferente, assim como a experiência oferecida pelos diferentes servidores, WPGraphQL ou Gato GraphQL.

Este artigo é a minha visão sobre o assunto, respondendo a vários dos comentários do post no Reddit.

GraphQL vs WP REST API

[Que péssima ideia] ter uma API GraphQL em cima do WordPress, que já usa sua própria REST API. Use a REST API. [Source]

Tanto a REST API quanto o GraphQL servem ao mesmo propósito: fornecer à aplicação os dados de que ela precisa. No entanto, eles se comportam de forma diferente em como alcançam isso: enquanto REST tem endpoints predefinidos que fornecem um conjunto específico de dados, GraphQL pode fornecer exatamente os dados necessários.

Esse comportamento diferente pode ter um impacto direto no desempenho da aplicação. Com REST, se precisamos buscar uma lista de posts mais alguns dados de cada autor do post, isso exigirá o envio de requisições extras. Possivelmente 1 requisição extra para todos os dados dos autores, ou 1 requisição extra por autor. Enquanto isso, o visitante do site pode estar esperando a página ser renderizada.

GraphQL melhora essa situação, pois podemos buscar diretamente todos os dados de posts e autores em uma única requisição, e a renderização da página será mais rápida:

{
  posts {
    id
    title
    excerpt
    date
    url
    author {
      id
      name
      url
    }
  }
}

Portanto, mesmo que já tenhamos a REST API no WordPress, isso não significa que ela seja sempre a ferramenta mais adequada para cada tarefa. Claro, sempre podemos usá-la, mas se também tivermos acesso ao GraphQL, podemos decidir usar essa API sempre que ela oferecer uma vantagem sobre REST, e sairemos ganhando.

Configuração inicial difícil para GraphQL + Ter que escrever resolvers

Certamente há um argumento a ser feito de que a configuração inicial para GraphQL é exponencialmente maior do que para REST; você está correto de que as associações precisam ser configuradas. [Source]

E...

O que você e quase todos os outros na web estão deixando de fora é que para que este formato de API funcione, você precisa escrever o parser (resolvers + tipos), o que traz uma série de problemas que não estão presentes com REST. [Source]

Esses comentários não são completamente precisos, porque tanto WPGraphQL quanto Gato GraphQL já mapearam o modelo de dados do WordPress no schema GraphQL (WPGraphQL completamente, meu plugin em sua maior parte).

Então, após instalar qualquer um desses plugins, você pode começar imediatamente a buscar dados para sua aplicação, sem precisar criar nenhum resolver, ou ter que configurar associações entre entidades.

É verdade que, para buscar dados personalizados das próprias entidades da aplicação (como de CPTs), elas precisam ser mapeadas via resolvers, e você precisará fazer isso. Mas isso não é diferente do REST: se você precisa de dados personalizados do seu CPT, precisará criar um endpoint REST para buscar esses dados personalizados. Um endpoint personalizado também é um resolver.

Portanto, no que diz respeito à necessidade de resolvers, REST e a API GraphQL são praticamente iguais.

Agora, navegando por sites e documentações, dá a impressão de que GraphQL requer mais esforço para configurar. Então há veracidade nessa presunção.

Acredito que há algumas razões para isso. Em primeiro lugar, GraphQL envolve (pelo menos) duas partes:

  1. o conceito do que é, e como funciona
  2. os servidores que fornecem alguma implementação concreta

Ao navegar pela documentação do GraphQL, como o site oficial graphql.org, ela se concentra nos conceitos por trás do GraphQL, entrando em detalhes sobre resolvers, o que são e por que são necessários.

Isso é útil quando você está construindo uma aplicação do zero, como ao usar Laravel e Lighthouse. Nesse caso, você precisa codificar seus resolvers (mas da mesma forma precisaria criar seus endpoints REST).

No entanto, o WordPress já é a aplicação, e WPGraphQL e Gato GraphQL são soluções. Esses dois plugins já criaram os resolvers para nós, então não precisamos nos preocupar com eles (de forma similar à WP REST API que também fornece um conjunto inicial de endpoints, então não precisamos nos preocupar com eles).

Além disso, GraphQL é mais orientado a desenvolvedores, e sua documentação parece falar diretamente com desenvolvedores. Desenvolvedores criam os resolvers no lado do servidor, e desenvolvedores consomem esses resolvers com queries personalizadas no lado do cliente. Como construir resolvers é uma tarefa para desenvolvedores, isso aparece naturalmente e com frequência.

Para REST, a expectativa (acredito) é que o endpoint fornecendo os dados necessários já exista (como fornecido pela WP REST API). Se não existir, só então precisamos nos preocupar em configurar um endpoint personalizado. Portanto, há menos ênfase na criação de resolvers para REST.

Então, ao final, tanto REST quanto GraphQL fornecem os dados necessários. Mas enquanto REST encoraja uma abordagem estática, onde os endpoints já deveriam existir, e só quando não existem nos preocupamos com eles, GraphQL encoraja uma abordagem dinâmica, onde cada query é feita sob medida, e então podemos codificar o resolver perfeito para ela.

Portanto, no fim das contas, não há diferenças fundamentais entre REST e GraphQL, apenas interpretações diferentes sobre como devem satisfazer seus requisitos.

Vulnerabilidades + Considerações de segurança no GraphQL

Um dia veremos uma grande vulnerabilidade do GraphQL porque escrever interpretadores seguros é realmente difícil. [Source]

E...

O WordPress já é tão massivo que já tem um enorme alvo nas costas; adicionar QUALQUER plugin acrescenta muito risco, e um plugin que se oferece para expor literalmente todo o WordPress, incluindo muitos exemplos de código para contornar o modelo de segurança, é um grande não para mim. O output não guiado pelo tema deve ser o mais restrito possível (inexistente a menos que eu solicite) além do que é absolutamente necessário expor. Espero que isso nunca entre no core. [Source]

GraphQL de fato impõe riscos de segurança adicionais que precisamos enfrentar. Concordo plenamente com essa percepção.

Mas não acho que seja um problema tão bloqueante, a ponto de impedir uma potencial inclusão do GraphQL no core do WP. Além disso, não acho nem mesmo que seja realmente difícil de resolver.

O que é necessário é que o servidor GraphQL se apoie nos mecanismos de segurança existentes do WordPress, e então que o desenvolvedor use esses mecanismos, garantindo que um determinado campo só possa ser acessado pelos usuários apropriados:

  • o usuário está logado?
  • o usuário é o administrador?
  • o usuário tem algum papel ou capacidade?
  • o usuário é o autor do post?

Para atender a essa proposta, Gato GraphQL oferece Listas de Controle de Acesso, para que possamos definir quem pode acessar cada campo e diretiva, e por configuração.

Agora, às vezes usar apenas uma ACL não é suficiente, e o servidor GraphQL precisa fornecer medidas de segurança extras. Descreverei o que estou trabalhando agora para a próxima v0.8 do Gato GraphQL.

O campo posts (para recuperar dados de posts) não requer autorização, qualquer usuário pode acessá-lo, seja logado ou não. Portanto, por razões de segurança, ele busca apenas posts publicados.

Mas há situações em que precisamos recuperar também posts em rascunho/pendente/lixeira, como:

  • Para construir um site estático, que é executado pelo administrador, com acesso a todos os dados do site
  • Para autores do post, para listar todos os rascunhos para que possam continuar editando-os

Então, elaborei o seguinte esquema. Para buscar posts, haverá 3 campos:

  • posts: aberto a qualquer um, só pode buscar posts publicados
  • myPosts: aberto a qualquer um, busca apenas posts do usuário logado, com qualquer status (publicado/rascunho/pendente/lixeira)
  • postsForAdmin: apenas o administrador pode acessá-lo, busca qualquer post com qualquer status

E então, postsForAdmin está desabilitado por padrão, portanto nem aparece no schema GraphQL, a menos que o administrador o habilite explicitamente (e, muito provavelmente, será habilitado apenas para construir sites estáticos).

Outra situação é quando um determinado campo pode recuperar tanto dados públicos quanto privados. Por exemplo, o campo option recupera dados da tabela wp_options. Algumas entradas são públicas (como blogname), enquanto outras não são (como admin_email).

Uma situação similar ocorre para recuperar valores meta, através dos campos Post.metaValue, User.metaValue, e outros. Por exemplo, o meta do usuário inclui a entrada wp_capabilities, que é certamente privada, enquanto description é pública. E então há last_name, que pode ser público ou privado dependendo da aplicação.

Para tornar o acesso a esses dados seguro, o plugin permitirá especificar quais entradas podem ser consultadas via uma lista de permissão/negação na página de configurações, aceitando tanto a entrada completa quanto uma regex:

Definindo entradas permitidas/negadas para o campo 'option'

Então, consultar a opção permitida funcionará, enquanto a opção negada simplesmente retornará null:

{
  # This option is allowed
  siteName: optionValue(name: "blogname")
  # This optionValue is not allowed
  adminEmail: optionValue(name: "admin_email")
}

Com medidas de segurança adequadas fornecidas pelo servidor GraphQL, e bom senso por parte do desenvolvedor, criar uma API GraphQL segura não deve ser difícil.

GraphQL derrubando o banco de dados

GraphQL é uma sintaxe rica que permite expressar queries relacionais profundas, então para um ecossistema como o WordPress, onde a extensibilidade do modelo de dados vem do padrão entity-attribute-value, isso se traduz em quantidades incríveis de desgaste em um banco de dados, o que pode tornar seu site não responsivo se a query GraphQL for profunda, complicada ou recursiva. O WordPress já é famoso por ser capaz de derrubar uma instância MySQL/MariaDB, então adicionar GraphQL poderia piorar muito as coisas se as queries não forem devidamente escritas, autenticadas e com limite de taxa. [Source]

Derrubar o banco de dados é uma preocupação séria para servidores GraphQL. Descreverei como o Gato GraphQL tenta evitar esse cenário.

Gato GraphQL evita que o problema N+1 ocorra, já por design arquitetural. Ele consegue isso fazendo com que o motor seja responsável pelo carregamento das entidades do banco de dados, não o desenvolvedor.

Ao resolver conexões em um resolver, o valor retornado é o ID (ou lista de IDs) do(s) objeto(s), e não o próprio objeto. Por exemplo, recuperar o autor do custom post é feito assim:

class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
  private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
 
  public function getClassesToAttachTo(): array
  {
    return [
      CustomPostFieldInterfaceResolver::class,
    ];
  }
 
  public function getSchemaFieldType(string $fieldName): ?string
  {
    return match($fieldName) {
      'author' => SchemaDefinition::TYPE_ID,
      default => null,
    };
  }
 
  public function resolveValue(
    TypeResolverInterface $typeResolver,
    object $customPost,
    string $fieldName,
    array $fieldArgs = []
  ): mixed {
    switch ($fieldName) {
      case 'author':
        return $this->customPostUserTypeAPI->getAuthorID($customPost);
    }
 
    return null;
  }
 
  public function resolveFieldTypeResolverClass(
    TypeResolverInterface $typeResolver,
    string $fieldName
  ): ?string {
    switch ($fieldName) {
      case 'author':
        return UserTypeResolver::class;
    }
 
    return null;
  }
}

Tendo o ID da entidade do banco de dados de resolveValue, e o tipo do objeto de resolveFieldTypeResolverClass (representado pela classe UserTypeResolver), o motor GraphQL pode então carregar os dados do objeto.

Para carregar os dados, o motor usa um algoritmo super eficiente: ele tem complexidade de tempo O(n), onde n é o número de tipos na query, não o número de nós.

O algoritmo alcança essa eficiência porque não percorre um grafo, mas converte a estrutura de dados em uma pilha de componentes, que é muito mais simples de resolver. (O "graph" em GraphQL é um conceito, não uma implementação concreta.)

Então, mesmo que a query tenha múltiplos níveis, cada um recuperando muitas entidades, o algoritmo ainda consegue suportá-la muito bem. Por exemplo, não há grande impacto ao executar a seguinte query, que tem uma profundidade de 10 níveis:

{
  posts(pagination: { limit: 10 }) {
    excerpt
    title
    url
    author {
      name
      url
      posts(pagination: { limit: 10 }) {
        title
        tags(pagination: { limit: 10 }) {
          slug
          url
          posts(pagination: { limit: 10 }) {
            title
            comments(pagination: { limit: 10 }) {
              content
              date
              author {
                name
                posts(pagination: { limit: 10 }) {
                  title
                  url
                  comments(pagination: { limit: 10 }) {
                    content
                    date
                    author {
                      name
                      username
                      url
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

A exceção a essa eficiência é ao recuperar valores meta, através de Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue e PostCategory.metaValue (e também seu campo metaValues). Isso porque as funções do WordPress (get_post_meta, get_user_meta, etc) buscam dados para 1 ID por vez, o que significa que cada entidade exigirá uma chamada ao banco de dados para buscar seu valor meta. Como resultado, a resolução de valores meta escala com base no número de nós, não no número de tipos (o comentário do OP acerta em cheio, nesse aspecto).

Para evitar que agentes mal-intencionados usem e abusem dos campos meta, o Gato GraphQL (na v0.8) será distribuído com esses campos desabilitados por padrão. Então, o administrador precisa habilitá-los explicitamente e, ao fazer isso, pode colocar esses campos sob alguma Lista de Controle de Acesso, para que em nenhum momento o banco de dados corra risco de ataque.

O rate limiting também é uma ótima ideia, planejo suportá-lo em alguma versão futura.

E depois há a análise e imposição de limitações sobre a complexidade da query (como quantos níveis de profundidade ela tem). O servidor GraphQL resolve a query com complexidade de tempo O(n), portanto não há muito dano que possa ser feito em relação a loops. No entanto, uma única query ainda poderia recuperar quantidades ilimitadas de dados do banco de dados, e isso é algo que podemos querer evitar.

Por exemplo, esta query simples trará uma enorme quantidade de dados em uma única requisição (meu site de demonstração mal tem algumas centenas de registros, então posso me dar ao luxo de demonstrar a execução da query):

{
  posts000: posts(pagination: { limit: 100 }) {
    ...PostFields
  }
  posts100: posts(pagination: { limit: 100, offset: 100 }) {
    ...PostFields
  }
  posts200: posts(pagination: { limit: 100, offset: 200 }) {
    ...PostFields
  }
  posts300: posts(pagination: { limit: 100, offset: 300 }) {
    ...PostFields
  }
  posts400: posts(pagination: { limit: 100, offset: 400 }) {
    ...PostFields
  }
  posts500: posts(pagination: { limit: 100, offset: 500 }) {
    ...PostFields
  }
  posts600: posts(pagination: { limit: 100, offset: 600 }) {
    ...PostFields
  }
  posts700: posts(pagination: { limit: 100, offset: 700 }) {
    ...PostFields
  }
  posts800: posts(pagination: { limit: 100, offset: 800 }) {
    ...PostFields
  }
  posts900: posts(pagination: { limit: 100, offset: 900 }) {
    ...PostFields
  }
}
 
fragment PostFields on Post {
  id
  title
  content
  date
}

Como se pode perceber, a query nem precisa ser aninhada para causar problemas. Portanto, analisar a complexidade de uma query é um assunto delicado, que exigirá ajuste fino para ser útil.

Espero suportar também a análise de queries, mas não está na minha lista de altas prioridades, porque com uma combinação das outras funcionalidades (como queries persistidas ou endpoints personalizados, combinados com Listas de Controle de Acesso) já podemos manter os agentes mal-intencionados afastados, e nós mesmos não iremos (não deveríamos!) abusar do nosso próprio serviço GraphQL.


Assine nossa newsletter

Fique por dentro de todas as atualizações do Gato GraphQL.