🤔 ¿Por qué tardó 1,5 años en lanzarse el nuevo Gato GraphQL?
La versión 0.9 de Gato GraphQL acaba de lanzarse. Tomó casi 1,5 años de desarrollo, y más de 16000 commits, estar lista. ¡Es realmente mucho tiempo!
Al compartir el anuncio en Hacker News, recibí la siguiente pregunta:
[...] Tengo curiosidad por saber qué llevó 16k commits. Los proyectos en los que he estado con más de diez mil commits tenían muchas docenas o cientos de personas trabajando a tiempo completo. [...] ¿Hay alguna complejidad que necesitaba ser superada que el post no menciona?
El número de commits no es una métrica muy fiable, ya que puedo hacer solo un cambio muy simple y empujarlo como un único commit. Muchos de esos 16k commits eran commits de "typo", o simplemente mejoraban una descripción en algún README.
No obstante, el conteo de commits sí da una idea del esfuerzo real involucrado. También hubo muchos commits cargados de modificaciones, incluyendo docenas, e incluso cientos de cambios a la vez. Los cambios entre las versiones 0.8 y 0.9 son realmente enormes, y eso tomó esfuerzo y tiempo para sacar adelante.
En esta entrada del blog, describiré cuáles son esos cambios, para explicar por qué llevó tanto tiempo. Y al hacerlo, también daré un avance de algunas características avanzadas que se añadieron al código base, y que verán la luz del día con la próxima versión 1.0.
Antecedentes del servidor GraphQL
Primero, compartiré un poco de la historia del motor, y detalles técnicos de cómo funciona.
(Esto es principalmente relevante para desarrolladores; si no te interesa lo técnico, eres bienvenido a saltar a la siguiente sección.)
Gato GraphQL se basa en PoP, un motor que renderiza componentes en PHP (similar a React o Vue en JavaScript). Su dependencia de este motor es absoluta, por lo que el plugin está alojado bajo el monorepo GatoGraphQL/GatoGraphQL en GitHub.
Bajo el capó, esta dependencia tiene este aspecto:
Gato GraphQL resuelve una consulta GraphQL primero transformándola en un modelo de componentes equivalente, que PoP luego resuelve obteniendo todos los datos requeridos, y luego a estos datos se les da la forma de la consulta GraphQL.
Cuando empecé a trabajar en PoP en algún momento alrededor de 2013/2014, no existía GraphQL, y la metodología para resolver un modelo de componentes en datos fue diseñada e implementada desde cero. La falta de tener un modelo a seguir (como GraphQL para conceptos, y el proyecto de referencia graphql-js para una implementación) ha sido tanto un obstáculo como una bendición, como explicaré más adelante.
PoP fue diseñado inicialmente para renderizar todo el sitio web como HTML en el lado del servidor, mientras que exponía los datos en bruto en formato JSON al añadir ?output=json a la URL de la página, y seleccionando más a fondo qué datos recuperar (settings, datos de objetos DB) con parámetros URL adicionales.
Por favor haz clic en los siguientes enlaces (todos apuntando a la misma página web, solo con distintos parámetros URL) y observa cómo se diferencian:
- Contenido HTML: mesym.com/en/posts/
- Datos JSON en bruto (settings + DB): mesym.com/en/posts/?output=json
- Datos JSON en bruto (DB): mesym.com/en/posts/?output=json&module=data
Al hacer clic en el último enlace, llega la realización: ¡Esto es prácticamente GraphQL! La única gran diferencia es que los datos en la respuesta están implícitos, ya que han sido ya definidos por los componentes (en PHP) que fueron incluidos en la página. GraphQL, en su lugar, nos permite decidir qué datos obtener vía una consulta.
Así que cuando aprendí sobre GraphQL en algún momento alrededor de 2019, fue obvio para mí hacer que PoP también satisficiera un servidor GraphQL. Todo lo que tenía que hacer era aceptar la consulta GraphQL como entrada, y crear un modelo de componentes al vuelo basado en la consulta.
Y eso es lo que hice. Y funcionó bien. Pero era lento, porque PoP entendía su propio formato de entrada, así que la consulta GraphQL tenía que adaptarse al formato de PoP:
- Parsear la consulta GraphQL; luego
- Transformar la consulta al formato PoP; luego
- Parsear el formato PoP
Parsear la consulta GraphQL se hacía entonces dos veces (una para GraphQL, una para PoP), y el formato PoP no se resolvía vía un AST, sino simplemente parseando la cadena de la consulta una y otra vez. (No usar un AST era una mala codificación, pero no tenía una spec que seguir, y su desarrollo ocurrió orgánicamente, donde un simple substr(...) salvaba el día, todos los días.)
Por eso digo que no tener la spec de GraphQL fue un obstáculo, ya que mi solución era lenta (y esa era la situación en la versión 0.8). Así que decidí arreglarlo.
Convirtiendo el motor en GraphQL-first
La solución que decidí es que PoP hable nativamente el lenguaje GraphQL. Entonces, pasar una consulta GraphQL a PoP como entrada ya se convertiría al modelo de componentes, sin la necesidad de ningún adaptador adicional, o hacer las cosas dos veces.
Esto significaba que el proyecto PoP tenía que reorientarse, de ser una librería PHP que renderiza componentes para sitios web en el lado del servidor que se adaptó para resolver consultas GraphQL, a convertirse de hecho en un servidor GraphQL.
El código base experimentó entonces una transformación masiva, introduciendo el AST de GraphQL como base para comunicar el estado a través de todos los servicios PHP en el motor. Los objetos AST de GraphQL son ahora las entradas a PoP (en lugar de las cadenas de consulta).
Otros servidores GraphQL en PHP se basan en graphql-php, pero el plugin Gato GraphQL no. Esto es una mala noticia respecto al esfuerzo de mantenimiento (ya que no puedo reusar lo que otra persona ha codificado), pero buenas noticias respecto a la independencia: puedo decidir añadir características personalizadas a mi plugin a mi propia velocidad, y bajo mi propio criterio (por eso el plugin ya proporciona el input object "oneof").
Y como se mostrará en la sección de abajo, esto es una gran ventaja.
Incorporando características originales a GraphQL
GraphQL normalmente se asocia con la obtención de datos. Naturalmente, puedes recuperar cualquier pieza de datos (posts, usuarios, comentarios, etc) de Gato GraphQL:
query {
posts(
pagination: { limit: 5, offset: 20 }
sort: { by: DATE, order: ASC }
) {
id
title
content
url
author {
id
name
url
}
comments {
id
date
content
}
}
}Pero esto es algo fácil. GraphQL también puede usarse para muchos otros casos de uso, incluyendo manipulación y transformación de datos, e incluso poner GraphQL en un pipeline para mediar entre servicios.
Algunos ejemplos donde GraphQL es útil:
- Extraer información de una o más fuentes (como usuarios de los sitios WordPress y los datos de contacto del newsletter de Mailchimp), combinando los datos, y analizándolos todos juntos como un único dataset
- Ejecutar operaciones para adaptar el contenido del sitio:
- Como una sola vez, como al migrar un sitio a otro dominio y reemplazar
"www.myoldsite.com"por"mynewsite.com"en todas partes del contenido y metadatos - De forma continua, como reemplazar cualquier
"http://"por"https://"cuando un escritor publica una nueva entrada
- Como una sola vez, como al migrar un sitio a otro dominio y reemplazar
- Conectarse a la API de Google Translate para traducir todas las entradas del blog a un idioma diferente
- Enviar un tweet automáticamente después de publicarse una entrada
PoP había sido diseñado para soportar estos otros casos de uso, vía características que no son (naturalmente) soportadas por GraphQL, como:
- Soportar campos de "funcionalidad" (además de campos de "datos"), que se añaden a todos los tipos en el esquema
- Pasar el resultado de un campo como entrada a otro campo, dentro de la misma consulta
- Componer directivas, para hacer que una directiva modifique el comportamiento de otra directiva
- Decidir aplicar una directiva o no dinámicamente, basándose en el valor del campo
Y ciertamente no quería eliminar estas características del servidor GraphQL: ya las había codificado, y son ciertamente valiosas.
Así que la segunda razón por la que v0.9 tardó tanto es que también tuve que encontrar una forma de incorporar estas nuevas capacidades a GraphQL, de una manera que no rompiera la spec de GraphQL (por ejemplo, introducir nuevos elementos a la sintaxis de GraphQL no era una opción).
Un ejemplo de manipulación de datos en GraphQL
Las nuevas capacidades introducidas en GraphQL en el plugin se harán más visibles en un futuro cercano, cuando se lance la versión 1.0. Pero ya puedes probar algunas de ellas.
La siguiente consulta GraphQL recupera una lista de entradas de usuario de una API REST externa (que puede ser @remove-ada de la respuesta); inyecta estos datos en otro campo, dentro de la misma consulta; extrae la propiedad email de cada entrada; y finalmente transforma el email a mayúsculas, pero solo si el idioma en esa misma entrada es inglés o alemán:
###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes
{
# Retrieve data from a REST API endpoint
userEntries: _sendJSONObjectCollectionHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
}
) # @remove # <= Uncomment this directive to not print the API data
emails: _echo(value: $__userEntries)
# Iterate all the entries, passing every entry
# (under the dynamic variable $userEntry)
# to each of the next 4 directives
@underEachArrayItem(
passValueOnwardsAs: "userEntry"
affectDirectivesUnderPos: [1, 2, 3, 4]
)
# Extract property "lang" from the entry
# via the functionality field `_objectProperty`,
# and pass it onwards as dynamic variable $userLang
@applyField(
name: "_objectProperty"
arguments: {
object: $userEntry,
by: {
key: "lang"
}
}
passOnwardsAs: "userLang"
)
# Execute functionality field `_inArray` to find out
# if $userLang is either "en" or "de", and place the
# result under dynamic variable $isSpecialLang
@applyField(
name: "_inArray"
arguments: {
value: $userLang,
array: ["en", "de"]
}
passOnwardsAs: "isSpecialLang"
)
# Extract property "email" from the entry
# and set it back as the value for that entry
@applyField(
name: "_objectProperty"
arguments: {
object: $userEntry,
by: {
key: "email"
}
}
setResultInResponse: true
)
# If $isSpecialLang is `true` then execute
# directive `@strUpperCase`
@if(condition: $isSpecialLang)
@strUpperCase
}Esta es la respuesta (por favor fíjate cómo solo ciertos emails fueron pasados a mayúsculas):
{
"data": {
"userEntries": [
{
"email": "abracadabra@ganga.com",
"lang": "de"
},
{
"email": "longon@caramanon.com",
"lang": "es"
},
{
"email": "rancotanto@parabara.com",
"lang": "en"
},
{
"email": "quezarapadon@quebrulacha.net",
"lang": "fr"
},
{
"email": "test@test.com",
"lang": "de"
},
{
"email": "emilanga@pedrola.com",
"lang": "fr"
}
],
"emails": [
"ABRACADABRA@GANGA.COM",
"longon@caramanon.com",
"RANCOTANTO@PARABARA.COM",
"quezarapadon@quebrulacha.net",
"TEST@TEST.COM",
"emilanga@pedrola.com"
]
}
}¡Compruébalo tú mismo! Pulsa el botón "Run" para ejecutar la consulta:
###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes {
# Retrieve data from a REST API endpoint
userEntries: _sendJSONObjectCollectionHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
}
)
# @remove # <= Uncomment this directive to not print the API data
emails: _echo(value: $__userEntries)
# Iterate all the entries, passing every entry
# (under the dynamic variable $userEntry)
# to each of the next 4 directives
@underEachArrayItem(
passValueOnwardsAs: "userEntry"
affectDirectivesUnderPos: [1, 2, 3, 4]
)
# Extract property "lang" from the entry
# via the functionality field `_objectProperty`,
# and pass it onwards as dynamic variable $userLang
@applyField(
name: "_objectProperty"
arguments: { object: $userEntry, by: { key: "lang" } }
passOnwardsAs: "userLang"
)
# Execute functionality field `_inArray` to find out
# if $userLang is either "en" or "de", and place the
# result under dynamic variable $isSpecialLang
@applyField(
name: "_inArray"
arguments: { value: $userLang, array: ["en", "de"] }
passOnwardsAs: "isSpecialLang"
)
# Extract property "email" from the entry
# and set it back as the value for that entry
@applyField(
name: "_objectProperty"
arguments: { object: $userEntry, by: { key: "email" } }
setResultInResponse: true
)
# If $isSpecialLang is `true` then execute
# directive `@strUpperCase`
@if(condition: $isSpecialLang)
@strUpperCase
}Había mencionado que no estar guiado por GraphQL fue un obstáculo, pero (en retrospectiva) también una bendición. Esto es porque no tenía las restricciones de la spec de GraphQL, así que podía permitirme soñar estas nuevas capacidades.
Y ahora que estas características han sido migradas a Gato GraphQL, puede ser un aliado increíblemente útil para cualquier cosa relacionada con la recuperación, manipulación y transformación de contenido para tu sitio WordPress. (Aunque solo serán accesibles con la próxima v1.0).
Llevó su tiempo, pero el esfuerzo ciertamente valió la pena.
¡Pruébalo!
¿Estás convencido de que la larga espera valió la pena? ¡Eso espero!
Adelante, descarga el plugin, y compruébalo:
¿Interesado en recibir noticias sobre su desarrollo, nueva documentación, y próximas releases, incluyendo v1.0? Entonces eres bienvenido a suscribirte al newsletter.
¿Quieres explorar el código open source en GitHub? Echa un vistazo a GatoGraphQL/GatoGraphQL (y sé bienvenido a darle una estrella... ¡Nos encantan las estrellas! ⭐️⭐️⭐️)
Por cierto, ¿qué transformaciones de contenido necesitas hacer en WordPress (para lo cual quizás ya estés usando algún plugin comercial dedicado)? Por favor envíame un mensaje contándome tu caso de uso.
Si te gusta lo que ves, por favor comparte con tus amigos y colegas, ayuda a difundir el amor ❤️.