Blog

🕸 Cómo y dónde puede GraphQL mejorar WordPress, complementando la REST API

Leonardo Losoviz
Por Leonardo Losoviz ·

Actualización 01/05/2024: Echa un vistazo a la comparación Gato GraphQL vs WP REST API.

El fin de semana pasado publiqué la entrada de blog 🦸🏿‍♂️ Gato GraphQL ahora se transpila de PHP 8.0 a 7.1.

Tras compartir el post en /r/php de Reddit, la comunidad inició una animada discusión sobre cuán útil es usar GraphQL en WordPress, cuán diferente es de la WP REST API, y cuán justificado está traer otra API más a WordPress.

Creo que la mayoría de los comentarios dan en el clavo, y a otros les falta algo de información clave. GraphQL no es solo una interfaz, sino también una implementación. Esto significa que diferentes servidores GraphQL, de diferentes proveedores, pueden haber sido diseñados para priorizar diferentes características. Como tal, no siempre podemos tener una expectativa unificada de lo que GraphQL ofrece, o una comprensión completa de cómo funciona un motor GraphQL.

Por ejemplo, la experiencia GraphQL en WordPress y en Laravel será diferente, así como la experiencia proporcionada por los diferentes servidores, WPGraphQL o Gato GraphQL.

Este artículo es mi opinión sobre el asunto, abordando varios de los comentarios del post de Reddit.

GraphQL vs WP REST API

[Tan mala idea] tener una API GraphQL encima de WordPress que ya usa su propia REST API. Simplemente usa la REST API. [Fuente]

Tanto la REST API como GraphQL cumplen el mismo propósito: proporcionar a la aplicación los datos que necesita. Sin embargo, se comportan de forma diferente en cómo lo logran: mientras que REST tiene endpoints predefinidos que proporcionan un conjunto específico de datos, GraphQL puede proporcionar exactamente los datos que se necesitan.

Este comportamiento diferente puede tener un impacto directo en el rendimiento de la aplicación. Con REST, si necesitamos obtener una lista de entradas además de algunos datos de cada autor de la entrada, eso requerirá enviar peticiones adicionales. Posiblemente 1 petición extra para todos los datos del autor, o 1 petición extra por autor. Mientras tanto, el visitante del sitio web puede estar esperando a que se renderice la página.

GraphQL mejora esta situación, ya que podemos obtener directamente todos los datos de la entrada y del autor en una única petición, y el renderizado de la página será más rápido:

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

Entonces, aunque ya tengamos la REST API en WordPress, no significa que sea siempre la herramienta más adecuada para cada tarea. Claro, siempre podemos usarla, pero si también tenemos acceso a GraphQL, entonces podemos decidir usar esta API siempre que proporcione una ventaja sobre REST, y estaremos mejor.

Configuración inicial difícil para GraphQL + Tener que escribir resolvers

Hay definitivamente un argumento a favor de que la configuración inicial para GraphQL es exponencialmente mayor que para REST; tienes razón en que las asociaciones tienen que configurarse. [Fuente]

Y...

Lo que tú y casi todos los demás en la web están omitiendo es que para que este formato de API funcione, tienes que escribir el parser (resolvers + tipos) que trae consigo una serie de problemas que no están presentes con REST. [Fuente]

Estos comentarios no son completamente precisos, porque tanto WPGraphQL como Gato GraphQL ya han mapeado el modelo de datos de WordPress en el esquema GraphQL (WPGraphQL completamente, mi plugin la mayoría de él).

Entonces, tras instalar cualquiera de estos plugins, puedes empezar inmediatamente a obtener datos para tu aplicación, sin necesidad de crear ningún resolver, o tener que configurar asociaciones entre entidades.

Es cierto que, para obtener datos personalizados de las entidades propias de la aplicación (como de los CPTs), estos necesitan ser mapeados vía resolvers, y tendrás que hacerlo. Pero esto no es diferente de REST: si necesitas datos personalizados de tu CPT, necesitarás crear un endpoint REST para obtener esos datos personalizados. Un endpoint personalizado también es un resolver.

Por lo tanto, respecto a la necesidad de resolvers, REST y la API GraphQL son prácticamente lo mismo.

Ahora, navegando por sitios web y documentación, sí da la impresión de que GraphQL requiere más esfuerzo de configuración. Así que hay algo de verdad en esta suposición.

Creo que hay algunas razones para esto. Por una parte, GraphQL involucra (al menos) dos partes:

  1. el concepto de qué es, y cómo funciona
  2. los servidores que proporcionan alguna implementación real

Al navegar por la documentación de GraphQL, como el sitio oficial graphql.org, se centra en los conceptos detrás de GraphQL, entrando en detalle sobre los resolvers, qué son y por qué se necesitan.

Esto es útil cuando estás construyendo una aplicación desde cero, como si usaras Laravel y Lighthouse. En ese caso, sí necesitas programar tus resolvers (pero también necesitarías crear tus endpoints REST).

Sin embargo, WordPress ya es la aplicación, y WPGraphQL y Gato GraphQL son soluciones. Estos dos plugins ya han creado los resolvers por nosotros, así que no necesitamos preocuparnos por ellos (de forma similar a que la WP REST API también proporciona un conjunto inicial de endpoints, así que no necesitamos preocuparnos por ellos).

Además, GraphQL está más centrado en desarrolladores, y su documentación parece hablar directamente a los desarrolladores. Los desarrolladores crean los resolvers en el lado del servidor, y los desarrolladores consumen esos resolvers con consultas personalizadas en el lado del cliente. Como construir resolvers es una tarea para desarrolladores, simplemente aparece de forma natural y a menudo.

Para REST, la expectativa (creo) es que el endpoint que proporcione los datos requeridos ya existirá (como lo entrega la WP REST API). Si no existe, solo entonces necesitamos preocuparnos por configurar un endpoint personalizado. Por lo tanto, hay menos énfasis en crear resolvers para REST.

Por lo tanto, tanto REST como GraphQL proporcionan los datos requeridos. Pero mientras REST anima a un enfoque estático, donde los endpoints ya deberían existir, y solo cuando no existen nos preocupamos por ellos, GraphQL anima a un enfoque dinámico, donde cada consulta se hace a medida, y entonces podemos programar el resolver perfecto para ella.

Así que, al final, no hay diferencias fundamentales entre REST y GraphQL, solo interpretaciones diferentes sobre cómo deben satisfacer sus requisitos.

Vulnerabilidades + Consideraciones de seguridad en GraphQL

Vamos a ver una vulnerabilidad enorme de GraphQL algún día porque escribir intérpretes seguros es realmente difícil. [Fuente]

Y...

WordPress ya es tan masivo que ya tiene un enorme objetivo en la espalda; añadir CUALQUIER plugin añade mucho riesgo, y un plugin que se ofrece a exponer literalmente todo WordPress, incluyendo muchas muestras de código para eludir el modelo de seguridad, es un gran no para mí. La salida no impulsada por el theme debería estar lo más restringida posible (inexistente a menos que la pida) más allá de lo absolutamente necesario para exponer. Espero que esto nunca llegue al core. [Fuente]

GraphQL sí impone riesgos de seguridad adicionales que necesitamos abordar. Estoy totalmente de acuerdo con este sentimiento.

Pero no creo que sea un problema tan bloqueante, como para impedir una posible inclusión de GraphQL en el core de WP. Además, ni siquiera creo que sea realmente difícil de abordar.

Lo que se necesita es que el servidor GraphQL aproveche los mecanismos de seguridad existentes de WordPress, y luego que el desarrollador use estos mecanismos, asegurándose de que algún campo solo pueda ser accedido por los usuarios apropiados:

  • ¿está el usuario logueado?
  • ¿es el usuario el admin?
  • ¿tiene el usuario algún rol o capability?
  • ¿es el usuario el autor de la entrada?

Para satisfacer esta propuesta, Gato GraphQL ofrece Control de acceso, de modo que podemos definir quién puede acceder a cada campo y directiva, y por configuración.

Ahora, a veces usar una ACL por sí sola no es suficiente, y el servidor GraphQL necesita proporcionar medidas de seguridad adicionales. Describiré en qué estoy trabajando ahora mismo para la próxima v0.8 de Gato GraphQL.

El campo posts (para recuperar datos de entradas) no requiere autorización, cualquier usuario puede acceder a él, esté logueado o no. Por lo tanto, por razones de seguridad, solo obtiene entradas publicadas.

Pero hay situaciones en las que necesitamos recuperar también entradas en borrador/pendientes/eliminadas, como:

  • Para construir un sitio web estático, que es ejecutado por el admin, con acceso a todos los datos del sitio
  • Para los autores de la entrada, para listar todas las entradas en borrador para que puedan seguir editándolas

Entonces, se me ocurrió el siguiente esquema. Para obtener entradas, habrá 3 campos:

  • posts: abierto a cualquiera, solo puede obtener entradas publicadas
  • myPosts: abierto a cualquiera, solo obtiene entradas del usuario logueado, con cualquier estado (publicada/borrador/pendiente/eliminada)
  • postsForAdmin: solo el admin puede acceder a él, obtiene cualquier entrada con cualquier estado

Y entonces, postsForAdmin está deshabilitado por defecto, así que ni siquiera aparece en el esquema GraphQL, a menos que el admin lo habilite explícitamente (y, lo más probable, se habilitará solo para construir sitios estáticos).

Otra situación es cuando algún campo puede recuperar tanto datos públicos como privados. Por ejemplo, el campo option recupera datos de la tabla wp_options. Algunas entradas son públicas (como blogname), mientras que otras no (como admin_email).

Una situación similar es para recuperar valores meta, a través de los campos Post.metaValue, User.metaValue, y otros. Por ejemplo, los meta del usuario incluyen la entrada wp_capabilities, que ciertamente es privada, mientras que description es pública. Y luego está last_name, que puede ser pública o privada dependiendo de la aplicación.

Para hacer que el acceso a estos datos sea seguro, el plugin permitirá especificar qué entradas pueden consultarse vía una allow/denylist en la página de ajustes, aceptando tanto la entrada completa como una regex:

Definir entradas permitidas/denegadas para el campo 'option'

Entonces, consultar la opción permitida funcionará, mientras que la opción denegada simplemente devolverá null:

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

Con medidas de seguridad adecuadas proporcionadas por el servidor GraphQL, y sentido común por parte del desarrollador, crear una API GraphQL segura no debería ser difícil.

GraphQL tumbando la BD

GraphQL es una sintaxis rica que permite expresar consultas relacionales profundas, así que para un ecosistema como WordPress, donde la extensibilidad del modelo de datos viene del patrón entity-attribute-value, esto se traduce en cantidades increíbles de desgaste sobre una base de datos, que puede causar que tu sitio deje de responder si la consulta GraphQL es profunda, complicada, o recursiva. WordPress ya es famoso por ser capaz de poner una instancia MySQL/MariaDB de rodillas, así que añadir GraphQL podría hacer esto mucho peor si las consultas no están adecuadamente escritas, autenticadas y rate limited. [Fuente]

Tumbar la BD es una preocupación seria para los servidores GraphQL. Describiré cómo Gato GraphQL intenta evitar este escenario.

Gato GraphQL evita que el problema N+1 ocurra, ya por diseño arquitectónico. Lo logra haciendo que el motor sea responsable de cargar las entidades desde la base de datos, no el desarrollador.

Al resolver conexiones en un resolver, el valor devuelto es el ID (o lista de IDs) del/los objeto(s), y no el objeto en sí. Por ejemplo, obtener el autor de la custom post se hace así:

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;
  }
}

Teniendo el ID de la entidad de BD desde resolveValue, y el tipo del objeto desde resolveFieldTypeResolverClass (representado vía la clase UserTypeResolver), el motor GraphQL puede entonces cargar los datos del objeto.

Para cargar los datos, el motor usa un algoritmo que es súper eficiente: tiene complejidad temporal O(n), donde n es el número de tipos en la consulta, no el número de nodos.

El algoritmo logra esta eficiencia porque no recorre un grafo, sino que convierte la estructura de datos en una pila de componentes, que es mucho más simple de resolver. (El "graph" en GraphQL es un concepto, no una implementación real.)

Entonces, aunque la consulta tenga múltiples niveles, cada uno recuperando muchas entidades, el algoritmo aún puede soportarlo bastante bien. Por ejemplo, no hay gran impacto al ejecutar la siguiente consulta, que tiene una profundidad de 10 niveles:

{
  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
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

La excepción a esta eficiencia es al recuperar valores meta, a través de Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue y PostCategory.metaValue (y también su campo metaValues). Eso es porque las funciones de WordPress (get_post_meta, get_user_meta, etc) obtienen datos para 1 ID a la vez, lo que significa que cada entidad requerirá una llamada a la base de datos para obtener su valor meta. Como resultado, resolver valores meta escala basándose en el número de nodos, no en el número de tipos (el comentario del OP da en el clavo, en este sentido).

Para evitar que actores maliciosos usen y abusen de los campos meta, Gato GraphQL (en v0.8) vendrá con estos campos deshabilitados por defecto. Entonces, el admin tiene que habilitarlos explícitamente y, al hacerlo, puede poner estos campos bajo alguna Access Control List, para que en ningún momento la BD esté en riesgo de ataque.

El rate limiting es una gran idea también, planeo soportarlo para alguna próxima release.

Y luego está analizar e imponer limitaciones sobre la complejidad de la consulta (como cuántos niveles de profundidad tiene). El servidor GraphQL resuelve la consulta con complejidad temporal O(n), así que no hay mucho daño que pueda hacerse respecto al loop. Sin embargo, una sola consulta podría aun así recuperar cantidades ilimitadas de datos de la BD, y eso es algo que podríamos querer evitar.

Por ejemplo, esta simple consulta traerá una cantidad enorme de datos en una sola petición (mi sitio de demo apenas tiene unos pocos cientos de registros, así que puedo permitirme demostrar la ejecución de la consulta):

{
  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 puede apreciarse, la consulta ni siquiera necesita estar anidada para crear problemas. Así que analizar la complejidad de una consulta es un asunto delicado, que requerirá afinarlo para ser útil.

Espero soportar el análisis de consultas también, pero no está en mi lista de altas prioridades, porque con una combinación de las otras características (como Persisted queries o Custom Endpoints, junto con Access Control Lists) ya podemos mantener fuera a los actores maliciosos, y nosotros mismos no (¡no deberíamos!) abusar de nuestro propio servicio GraphQL.


Suscríbete a nuestra newsletter

Mantente al tanto de todas las novedades de Gato GraphQL.