Conceptos, ideas, estrategias
Conceptos, ideas, estrategiasCómo el plugin mapea el modelo de datos de WordPress al esquema GraphQL

Cómo el plugin mapea el modelo de datos de WordPress al esquema GraphQL

Así es como Gato GraphQL ha mapeado el modelo de datos de WordPress a un esquema GraphQL correspondiente.

El modelo de datos de WordPress

WordPress tiene las siguientes entidades:

  • posts
  • páginas
  • custom posts
  • elementos multimedia
  • usuarios
  • roles de usuario
  • etiquetas
  • categorías
  • comentarios
  • bloques
  • propiedades meta
  • otros (opciones, plugins, temas, etc.)

Estas entidades pueden tener una jerarquía. Por ejemplo, post, página y elementos multimedia son todos custom post types, y etiquetas y categorías son ambas taxonomías.

Este es el diagrama de la base de datos de WordPress, que muestra cómo se almacenan los datos para todas las entidades:

Diagrama de la base de datos de WordPress

¿Es el mapeo una réplica exacta del diagrama de la BD?

Al mapear la base de datos de WordPress a un esquema GraphQL, ¿se respeta el mismo diagrama de arriba uno a uno?

No, no se respeta. Si bien el diagrama de la base de datos es una implementación real, GraphQL es una interfaz para acceder a los datos desde el cliente. Estos dos están relacionados, pero pueden ser diferentes. A GraphQL no le importa la base de datos: no piensa en comandos SQL, ni sabe que hay tablas llamadas wp_posts y wp_users.

Así que no nos tenemos que preocupar demasiado por el diagrama de la base de datos al crear el esquema GraphQL para WordPress. Es más, podemos producir un esquema GraphQL que corrija parte de la deuda técnica del modelo de datos de WordPress.

Mapear el modelo de datos de WordPress como un esquema GraphQL

Hagamos el mapeo. Primero, mapeamos las entidades originales como tipos, en la medida de lo posible. De la lista de entidades en el modelo de datos de WordPress, producimos los siguientes tipos para el esquema GraphQL:

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

Después, añadimos todos los campos esperados a cada tipo. Para representar el esquema, podemos usar SDL, o Schema Definition Language. (Esto es solo para documentación; el plugin en sí no usa SDL para codificar el esquema: todo es código PHP).

Estos son los campos (entre muchos otros) para un Post:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
  date: Date!
}

Estos son los campos (entre muchos otros) para un User:

type User {
  id: ID!
  name: String
  email: String!
}

También creamos las conexiones correspondientes, que son campos que devuelven otra entidad (en lugar de un escalar, como un número o una cadena). Por ejemplo, representamos que un post tiene un autor, y que un usuario es dueño de posts:

type Post {
  author: User!
}
 
type User {
  posts: [Post]
}

Los campos y las conexiones también pueden aceptar argumentos. Por ejemplo, habilitamos que Post.dateStr se pueda formatear, y que User.posts pueda filtrar entradas, limitar su número y ordenarlas:

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
}
 
# ...

Seguimos haciendo esto con todas las entidades del modelo de datos de WordPress. Una vez terminado, llegaremos al esquema GraphQL para WordPress, visible mediante el cliente Voyager (disponible como "Interactive Schema" en el menú del plugin):

El esquema GraphQL para WordPress

Este esquema tiene similitudes con el diagrama de la base de datos de WordPress, pero también varias diferencias. Analicémoslas.

Las operaciones sin entidad se mapean como campos Root

El diagrama de la base de datos de WordPress representa cómo se almacenan los datos, por lo que no hay un "comienzo". Sin embargo, GraphQL es una interfaz para recuperar datos, así que debe haber una etapa inicial desde la que ejecutar la consulta.

Esta etapa inicial es el tipo Root, o, para ser más precisos, los tipos QueryRoot y MutationRoot (para tratar con consultas y mutaciones, respectivamente).

En estos dos tipos, mapeamos todas las operaciones que no dependen de una entidad, como al ejecutar get_posts(), get_users() o wp_signon():

type QueryRoot {
  posts: [Post]!
  users: [User]!
}
 
type MutationRoot {
  loginUser(
    usernameOrEmail: String!,
    password: String!
  ): User
}

Los campos no necesitan tener el mismo nombre o firma que la operación que representan. Por ejemplo, llamar al campo loginUser puede considerarse más adecuado que signOn.

Agrupando elementos del esquema

Podemos aplicar mejoras para simplificar el esquema y hacerlo más útil. Por ejemplo, un campo puede recibir todos sus argumentos mediante un objeto de entrada, que puede reutilizarse entre varios campos y facilita la visualización del esquema:

type MutationRoot {
  loginUser(input: LoginUserByInput!): User
}
 
input LoginUserByInput {
    usernameOrEmail: String!,
    password: String!
}

Además, la respuesta de una mutación puede ser un objeto "payload", que además de devolver el objeto afectado también puede incluir el estado de la operación y mensajes de error:

type MutationRoot {
  loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
 
type RootLoginUserMutationPayload {
  errors: [RootLoginUserMutationErrorPayloadUnion!]
  status: OperationStatusEnum!
  user: User
  userID: ID
}
 
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
  | InvalidUserEmailErrorPayload
  | InvalidUsernameErrorPayload
  | PasswordIsIncorrectErrorPayload
  | UserIsLoggedInErrorPayload

Todas las mutaciones van bajo MutationRoot

Hay operaciones que sí dependen de una entidad, como wp_update_post(), que se aplica sobre algún post. La mutación correspondiente en el esquema GraphQL debe añadirse al tipo MutationRoot, porque así es como funciona GraphQL.

Entonces, esta operación se mapea así:

type MutationRoot {
  updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
 
input RootUpdatePostFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  id: ID!
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Este plugin también soporta mutaciones anidadas, que se ofrecen como característica opt-in (porque no es un comportamiento estándar de GraphQL). Entonces, las mutaciones también pueden añadirse bajo cualquier tipo, no solo MutationRoot. En este caso, obtenemos:

type Post {
  update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
 
input PostUpdateFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Hay que notar la diferencia entre los inputs RootUpdatePostFilterInput y PostUpdateFilterInput (es decir, entre mutaciones desde la raíz, y mutaciones anidadas): el primero tiene la propiedad obligatoria id para indicar qué post modificar, pero el segundo no, ya que no la necesita.

Tratando con custom posts

No hay herencia de tipos en GraphQL. Por tanto, no podemos tener un tipo CustomPost y declarar que Post y Page lo extienden.

GraphQL ofrece dos recursos para compensar esta carencia: interfaces y union types.

Para el primero, creamos una interfaz CustomPost para el esquema, declarando todos los campos esperados de un custom post, y definimos los tipos Post, Page y GenericCustomPost (para representar todos los custom post types definidos por cualquier tema y plugin instalado) para implementar la interfaz:

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 el segundo, creamos un tipo CustomPostUnion para el esquema que devuelve todos los custom post types:

union CustomPostUnion = Post | Page | GenericCustomPost

Y hacemos que los campos devuelvan este tipo siempre que sea apropiado:

type QueryRoot {
  customPost(id: ID): CustomPostUnion
  customPosts: [CustomPostUnion]!
}
 
type User {
  customPosts: [CustomPostUnion]
}
 
type Comment {
  customPost: CustomPostUnion!
}

Al ejecutar la consulta, podemos seleccionar los campos basándonos en el tipo real, como Post, o en la interfaz CustomPost:

{  
  customPosts {
    __typename
    ...on CustomPost {
      id
      title
      slug
      status
    }
    ...on Post {
      isSticky
      postFormat
    }
  }
}

Como se puede observar, en el esquema GraphQL tenemos que afirmar explícitamente cuándo tratamos con posts y cuándo con custom posts, ya que ¡no son lo mismo! Llamar a estos dos de forma intercambiable es deuda técnica de WordPress, que el plugin intenta arreglar siempre que sea posible.

Por esta razón, un custom post se llama siempre CustomPost y no Post, un campo que trata con custom posts se llama siempre customPosts y no posts, y un argumento de campo que recibe el ID de un custom post se llama customPostID y no postID (aunque así se llame en la función mapeada de WordPress).

Entonces, la expectativa es siempre clara:

  • El campo User.customPosts puede devolver una lista de cualquier custom post, incluyendo posts y páginas, y User.posts solo devuelve posts
  • El campo Root.setFeaturedImageOnCustomPost puede añadir una imagen destacada a cualquier custom post, por eso no se llama setFeaturedImageOnPost

No agrupar etiquetas (y categorías) bajo un único tipo

¿Por qué el tipo PostTag (y lo mismo para PostCategory) se llama así, en lugar de simplemente Tag?

Porque, al ejecutar esta consulta (donde un producto es un CPT), los resultados del campo tags para posts y productos serán siempre diferentes, sin solapamiento:

query {
  posts {
    tags {
      id
      name
    }
  }
  products {
    tags {
      id
      name
    }
  }
}

Las etiquetas añadidas a posts no aparecerán al recuperar etiquetas para productos, y al revés (a menos que un producto también use la taxonomía post_tag, pero entonces también puede representarse con el tipo PostTag). Esto no representa un gran problema en WordPress, ya que estos elementos pueden considerarse filas diferentes de la misma tabla de la base de datos. Pero sí importa para GraphQL, que está fuertemente tipado.

Entonces, es una buena decisión de diseño mantener estas entidades separadas, bajo sus propios tipos, y que las etiquetas para posts se devuelvan bajo el tipo PostTag y, si un plugin personalizado implementa su propio CPT de producto, debe usar el tipo ProductTag para sus etiquetas.

Dándoles a los elementos multimedia su propia identidad

Las entidades de medios en WordPress son custom post types, solo porque era conveniente desde un punto de vista de implementación. Sin embargo, el esquema GraphQL puede evitar esta deuda técnica, y modelar los elementos multimedia como una entidad distinta, no como custom posts.

Esto implica las siguientes decisiones para el esquema GraphQL:

  • El tipo Media no implementa la interfaz CustomPost, y no formará parte del tipo CustomPostUnion
  • El tipo Media no tiene muchos campos esperados de un custom post type, como excerpt, date y status. En su lugar, solo tiene los campos esperados de un elemento multimedia:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Identificación y mapeo de enums

En algunas situaciones, WordPress usa valores fijos de un conjunto dado. Por ejemplo, el estado de un post solo puede ser "publish", "draft", "pending" o "trash".

En GraphQL, podemos tratar estos como enums (en lugar de strings), y crear un tipo de enumeración correspondiente. Siguiendo el estándar GraphQL, los enums deben escribirse en mayúsculas, así:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Sin embargo, entonces la consulta no se puede usar directamente para interactuar con WordPress, ya que ejecutar get_posts( [ "post_status" => "PUBLISH" ] ) no funciona.

Entonces, como compromiso, mantenemos estos valores enum en minúsculas:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Mapeo de tipos adicionales

Los bloques no son directamente visibles en el diagrama de la base de datos de WordPress, ya que se almacenan en wp_posts (no hay tabla wp_blocks), pero, no obstante, son una entidad distinta.

Por tanto, todavía podemos introducir un tipo Block para mapearlos:

type Post {
  blocks: [Block]
}
 
type Block {
  type: String!
  attributes: JSONObject
}