Como o plugin mapeia o modelo de dados WordPress no schema GraphQL
Veja como o Gato GraphQL mapeou o modelo de dados WordPress em um schema GraphQL correspondente.
O modelo de dados WordPress
O WordPress possui as seguintes entidades:
- posts
- pages
- custom posts
- elementos de mídia
- usuários
- papéis de usuário
- tags
- categorias
- comentários
- blocos
- propriedades meta
- outros (opções, plugins, temas, etc.)
Essas entidades podem ter uma hierarquia. Por exemplo, post, page e elementos de mídia são todos custom post types, e tags e categorias são ambas taxonomias.
Este é o diagrama do banco de dados WordPress, mostrando como os dados de todas as entidades são armazenados:

O mapeamento é uma réplica exata do diagrama do banco de dados?
Ao mapear o banco de dados WordPress em um schema GraphQL, o mesmo diagrama acima é respeitado 1 a 1?
Não, não é. Enquanto o diagrama do banco de dados é uma implementação real, o GraphQL é uma interface para acessar os dados a partir do cliente. Esses dois elementos são relacionados, mas podem ser diferentes. O GraphQL não se preocupa com o banco de dados: ele não pensa em comandos SQL, nem sabe que existem tabelas de banco de dados chamadas wp_posts e wp_users.
Portanto, não precisamos nos preocupar muito com o diagrama do banco de dados ao criar o schema GraphQL para o WordPress. Aliás, podemos produzir um schema GraphQL que corrige parte da dívida técnica do modelo de dados WordPress.
Mapeando o modelo de dados WordPress como um schema GraphQL
Vamos fazer o mapeamento. Primeiro, mapeamos as entidades originais como tipos, na medida do possível. A partir da lista de entidades no modelo de dados WordPress, produzimos os seguintes tipos para o schema GraphQL:
PostPageMediaUserUserRolePostTagPostCategoryComment
Em seguida, adicionamos todos os campos esperados a cada tipo. Para representar o schema, podemos usar a SDL, ou Schema Definition Language. (Isso é usado apenas para fins de documentação; o próprio plugin não usa SDL para codificar o schema: tudo é código PHP).
Estes são os campos (entre muitos outros) para um Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
date: Date!
}Estes são os campos (entre muitos outros) para um User:
type User {
id: ID!
name: String
email: String!
}Também criamos as conexões correspondentes, que são campos que retornam outra entidade (em vez de um escalar, como um número ou uma string). Por exemplo, representamos um post tendo um autor, e um usuário possuindo posts:
type Post {
author: User!
}
type User {
posts: [Post]
}Campos e conexões também podem aceitar argumentos. Por exemplo, permitimos que Post.dateStr seja formatado, e que User.posts filtre entradas, limite sua quantidade e as ordene:
type Post {
dateStr(format: String): Date!
}
type User {
posts(
filter: RootPostsFilterInput
pagination: PostPaginationInput
sort: CustomPostSortInput
): [Post!]!
}
input RootPostsFilterInput {
authorIDs: [ID!]
authorSlug: String
categoryIDs: [ID!]
dateQuery: [DateQueryInput!]
excludeAuthorIDs: [ID!]
excludeIDs: [ID!]
hasPassword: Boolean = false
ids: [ID!]
isSticky: Boolean
metaQuery: [CustomPostMetaQueryInput!]
password: String
search: String
status: [FilterCustomPostStatusEnum!]
tagIDs: [ID!]
tagSlugs: [String!]
}
input PostPaginationInput {
limit: Int
offset: Int
}
input CustomPostSortInput {
by: CustomPostOrderByEnum
order: OrderEnum
}
# ...Continuamos fazendo isso para todas as entidades no modelo de dados WordPress. Quando terminarmos, chegaremos ao schema GraphQL para o WordPress, visível usando o cliente Voyager (disponível como "Interactive Schema" no menu do plugin):

Este schema tem semelhanças com o diagrama do banco de dados WordPress, mas também várias diferenças. Vamos analisá-las.
Operações sem entidade são mapeadas como campos Root
O diagrama do banco de dados WordPress representa como os dados são armazenados, portanto não há um "início". O GraphQL, porém, é uma interface para recuperar dados, por isso deve haver uma etapa inicial a partir da qual executar a query.
Essa etapa inicial é o tipo Root, ou, para ser mais preciso, os tipos QueryRoot e MutationRoot (para lidar com queries e mutations, respectivamente).
Nesses dois tipos, mapeamos todas as operações que não dependem de uma entidade, como ao executar get_posts(), get_users() ou wp_signon():
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
loginUser(
usernameOrEmail: String!,
password: String!
): User
}Os campos não precisam ter o mesmo nome ou assinatura que a operação que representam. Por exemplo, chamar o campo loginUser pode ser considerado mais adequado do que signOn.
Agrupamento de elementos do schema
Podemos aplicar melhorias para simplificar o schema e torná-lo mais útil. Por exemplo, um campo pode receber todos os seus argumentos por meio de um objeto input, que pode ser reutilizado em vários campos e facilita a visualização do schema:
type MutationRoot {
loginUser(input: LoginUserByInput!): User
}
input LoginUserByInput {
usernameOrEmail: String!,
password: String!
}Além disso, a resposta de uma mutation pode ser um objeto "payload", que, além de retornar o objeto afetado, também pode incluir o status da operação e mensagens de erro:
type MutationRoot {
loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
type RootLoginUserMutationPayload {
errors: [RootLoginUserMutationErrorPayloadUnion!]
status: OperationStatusEnum!
user: User
userID: ID
}
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
| InvalidUserEmailErrorPayload
| InvalidUsernameErrorPayload
| PasswordIsIncorrectErrorPayload
| UserIsLoggedInErrorPayloadTodas as mutations ficam sob MutationRoot
Existem operações que dependem de uma entidade, como wp_update_post(), que é aplicada em algum post. A mutation correspondente no schema GraphQL deve ser adicionada ao tipo MutationRoot, porque é assim que o GraphQL funciona.
Então, essa operação é mapeada assim:
type MutationRoot {
updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
input RootUpdatePostFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
id: ID!
status: CustomPostStatusEnum
tags: [String!]
title: String
}Este plugin também suporta mutations aninhadas, oferecidas como uma funcionalidade opt-in (pois não é um comportamento GraphQL padrão). Assim, mutations também podem ser adicionadas sob qualquer tipo, não apenas MutationRoot. Nesse caso, obtemos:
type Post {
update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
input PostUpdateFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
status: CustomPostStatusEnum
tags: [String!]
title: String
}Observe a diferença entre os inputs RootUpdatePostFilterInput e PostUpdateFilterInput (ou seja, entre mutations a partir da raiz e mutations aninhadas): o primeiro tem a propriedade obrigatória id para indicar qual post modificar, mas o segundo não, pois não precisa dela.
Lidando com custom posts
Não há herança de tipos no GraphQL. Portanto, não podemos ter um tipo CustomPost e declarar que Post e Page o estendem.
O GraphQL oferece dois recursos para compensar essa ausência: interfaces e tipos union.
Para o primeiro, criamos uma interface CustomPost para o schema, declarando todos os campos esperados de um custom post, e definimos os tipos Post, Page e GenericCustomPost (para representar todos os custom post types definidos por qualquer tema e plugin instalado) para implementar a interface:
interface CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Post implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Page implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type GenericCustomPost implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}Para o segundo, criamos um tipo CustomPostUnion para o schema retornando todos os custom post types:
union CustomPostUnion = Post | Page | GenericCustomPostE fazemos com que os campos retornem esse tipo quando apropriado:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Ao executar a query, podemos selecionar os campos com base no tipo real, como Post, ou na interface CustomPost:
{
customPosts {
__typename
...on CustomPost {
id
title
slug
status
}
...on Post {
isSticky
postFormat
}
}
}Como pode ser observado, no schema GraphQL precisamos afirmar explicitamente quando estamos lidando com posts e quando com custom posts, pois eles não são a mesma coisa! Chamar esses dois de forma intercambiável é uma dívida técnica do WordPress, que o plugin tenta corrigir sempre que possível.
Por essa razão, um custom post é sempre chamado de CustomPost e não de Post, um campo que lida com custom posts é sempre chamado de customPosts e não de posts, e um argumento de campo que recebe o ID de um custom post é chamado de customPostID e não de postID (mesmo que seja assim que é chamado na função WordPress mapeada).
Assim, a expectativa é sempre clara:
- O campo
User.customPostspode retornar uma lista de qualquer custom post, incluindo posts e páginas, eUser.postsretorna apenas posts - O campo
Root.setFeaturedImageOnCustomPostpode adicionar uma imagem destacada a qualquer custom post, por isso não se chamasetFeaturedImageOnPost
Não agrupar tags (e categorias) sob um único tipo
Por que o tipo PostTag (e o mesmo vale para PostCategory) se chama assim, em vez de simplesmente Tag?
Porque, ao executar esta query (onde um produto é um CPT), os resultados do campo tags para posts e produtos sempre serão diferentes, sem sobreposição:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Tags adicionadas a posts não aparecerão ao recuperar tags de produtos, e vice-versa (a menos que um produto também use a taxonomia post_tag, mas então ele também pode ser representado com o tipo PostTag). Isso não representa um grande problema no WordPress, pois esses itens podem ser considerados linhas diferentes da mesma tabela de banco de dados. Mas importa para o GraphQL, que é fortemente tipado.
Portanto, é uma boa decisão de design manter essas entidades separadas, sob seus próprios tipos, e fazer com que tags de posts sejam retornadas sob o tipo PostTag e que, se um plugin personalizado implementar seu próprio CPT de produto, ele deva usar o tipo ProductTag para suas tags.
Dando identidade própria aos itens de mídia
Entidades de mídia no WordPress são custom post types, apenas porque era conveniente do ponto de vista de implementação. No entanto, o schema GraphQL pode evitar essa dívida técnica e modelar elementos de mídia como uma entidade distinta, não como custom posts.
Isso implica as seguintes decisões para o schema GraphQL:
- O tipo
Medianão implementa a interfaceCustomPoste não fará parte do tipoCustomPostUnion - O tipo
Medianão possui muitos campos esperados de um custom post type, comoexcerpt,dateestatus. Em vez disso, ele tem apenas os campos esperados de um elemento de mídia:
type Media {
id: ID!
src: String!
width: Int
height: Int
}Identificando e mapeando enums
Em algumas situações, o WordPress usa valores fixos de um conjunto determinado. Por exemplo, o status de um post só pode ser "publish", "draft", "pending" ou "trash".
No GraphQL, podemos tratar esses valores como enums (em vez de strings) e criar um tipo de enumeração correspondente. Seguindo o padrão GraphQL, enums devem ser escritos em maiúsculas, assim:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}No entanto, a query não pode ser usada diretamente para interagir com o WordPress, pois executar get_posts( [ "post_status" => "PUBLISH" ] ) não funciona.
Então, como compromisso, mantemos esses valores de enum em minúsculas:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Mapeando tipos adicionais
Blocos não são diretamente visíveis no diagrama do banco de dados WordPress, pois são armazenados em wp_posts (não há tabela wp_blocks), mas ainda assim são uma entidade distinta.
Portanto, ainda podemos introduzir um tipo Block para mapeá-los:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}