Conceitos, Ideias, Estratégias
Conceitos, Ideias, EstratégiasComo o plugin mapeia o modelo de dados WordPress no schema GraphQL

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 diagrama do banco de dados WordPress

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:

  • Post
  • Page
  • Media
  • User
  • UserRole
  • PostTag
  • PostCategory
  • Comment

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):

O schema GraphQL para WordPress

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
  | UserIsLoggedInErrorPayload

Todas 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 | GenericCustomPost

E 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.customPosts pode retornar uma lista de qualquer custom post, incluindo posts e páginas, e User.posts retorna apenas posts
  • O campo Root.setFeaturedImageOnCustomPost pode adicionar uma imagem destacada a qualquer custom post, por isso não se chama setFeaturedImageOnPost

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 Media não implementa a interface CustomPost e não fará parte do tipo CustomPostUnion
  • O tipo Media não possui muitos campos esperados de um custom post type, como excerpt, date e status. 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
}