👨🏻💻 GraphQL como (una especie de) lenguaje de programación
GraphQL, aun teniendo el lenguaje GraphQL, no se denominaría normalmente lenguaje de programación, ya que hay muchas cosas que podemos hacer con los lenguajes de programación que no podemos hacer con GraphQL.
GraphQL normalmente se usa para obtener datos, por ejemplo para renderizar un sitio web en el cliente, y para mutar datos, por ejemplo para crear una entrada. Y eso es prácticamente todo.
(Otros usos son simplemente combinaciones de estos 2 casos anteriores. Por ejemplo, una API gateway puede obtener/mutar datos de un servidor interno, que no está expuesto al cliente.)
Acceder a datos en GraphQL:
query PrintPostTitle($postID: ID!)
{
post(by: { id: $postID }) {
title
}
}...tiene este equivalente (más o menos) en PHP:
function printPostTitle(int $postID)
{
$post = getPost($postID);
echo $post->title;
}(Todos los ejemplos siguientes usarán PHP como lenguaje de programación para la comparación.)
Mutar datos en GraphQL:
query UpdatePost($postID: ID!, $title: String!)
{
updatePost(
by: { id: $postID },
input: { title: $title }
) {
title
}
}...tiene este equivalente (más o menos) en PHP:
function updatePost(int $postID, string $title)
{
$post = getPost($postID);
$post->update(['title' => $title]);
}Esto es suficiente porque GraphQL normalmente se accede desde un cliente (programado en algún lenguaje de programación, como JavaScript, PHP, Java, u otro) que contendrá la lógica de qué hacer con los datos. Así que GraphQL no se usa solo, sino como compañero de alguien más.
Pero si GraphQL pudiera usarse por sí solo, entonces muchos nuevos casos de uso podrían resolverse usando solo GraphQL, permitiendo a GraphQL ser desplegado en entornos novedosos y ser responsable de tareas adicionales en el stack de la aplicación.
Para que eso ocurra, sin embargo, GraphQL debe soportar muchas de las características de los lenguajes de programación.
Las características de lenguaje de programación que GraphQL soporta son limitadas. Por ejemplo, usar la directiva @include (o @skip) y pasar una variable como entrada puede considerarse (más o menos) lógica condicional:
query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Esta consulta tiene este equivalente en PHP:
function printPostProperties(int $postID, bool $addContent)
{
$post = getPost($postID);
echo $post->title;
if ($addContent) {
echo $post->content;
}
}Eso es prácticamente todo. GraphQL carece de recursiones, variables dinámicas (donde sus valores se calculan y asignan a la variable en tiempo de ejecución, no como entrada en el diccionario), asignaciones de variables (ej: asignar la salida de un campo a una variable, que luego puede proporcionarse como argumento a otro campo), y otros.
Considera cómo implementarías una solución, usando solo GraphQL, para el siguiente problema:
- Crear un webhook que sea invocado por un servicio cuando un nuevo usuario se registra en ese servicio; el usuario puede haberse suscrito al newsletter (indicado por el campo
marketing_optinen el payload del webhook); en ese caso, el webhook debe registrar el email del usuario (en el campoemailen el payload del webhook) en una lista de Mailchimp.
¿Te parece factible? ¿fácil? ¿difícil? ¿imposible?
En Gato GraphQL, queremos resolver este problema usando solo GraphQL. Y muchos más problemas. Por eso hemos pensado mucho en cómo soportar características de los lenguajes de programación.
Exploremos qué características de programación hemos soportado en nuestro servidor GraphQL. Al final de esta entrada, veremos cómo podemos resolver ese problema.
Funcionalidad
Los campos en GraphQL normalmente traen datos, como el título, contenido o datos de una entrada. Pero también podemos implementar campos como "funcionalidad".
Por ejemplo, imprimir la hora en PHP:
function printTime()
{
echo time();
}...puede hacerse con el campo _time en GraphQL:
{
_time
}Fíjate que la función time no pertenece a ningún tipo, por lo que el campo _time tampoco. Como tal, es un campo global, y puede accederse bajo cada tipo del esquema GraphQL:
{
posts {
_time
}
}Otros ejemplos de campos de funcionalidad son:
_arrayItem_arrayJoin_date_equals_inArray_intAdd_isEmpty_isNull_makeTime_objectProperty_sprintf_strContains_strRegexReplace_strSubstr
Funciones
Podemos dividir unidades de lógica en funciones, y hacer que una función invoque a otra función:
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
printPostContent();
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}En GraphQL, podemos análogamente dividir la operación query (o mutation) del documento en múltiples operaciones query, y hacer que una operación "dependa" de otras, ejecutando esas primero:
query PrintPostTitle($postID: ID!)
{
postWithTitle: post(by: { id: $postID }) {
title
}
}
query PrintPostContent($postID: ID!)
{
postWithContent: post(by: { id: $postID }) {
content
}
}
query PrintPostProperties
@depends(on: [
"PrintPostTitle",
"PrintPostContent"
])
{
# ...
}En esta consulta, ejecutar la consulta GraphQL pasando ?operationName=PrintPostProperties al endpoint ejecutará primero las consultas PrintPostTitle y PrintPostContent, y solo después PrintPostProperties.
Esto es posible vía Ejecución de múltiples consultas.
Variables Dinámicas
Podemos calcular un valor y asignarlo a una variable en tiempo de ejecución. Luego, basándonos en ese valor, podemos ejecutar condicionalmente alguna funcionalidad o no:
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = isUserLoggedIn();
if ($addContent) {
echo $post->content;
}
}En GraphQL, podemos "exportar" un valor bajo una variable dinámica en alguna operación, y luego leer este valor en otra operación:
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}Fíjate que la variable $addContent, que contiene un valor que se calculó en tiempo de ejecución, es leída pero no declarada en la operación PrintPostProperties, ya que es una variable dinámica.
Ejecutar funciones condicionalmente
Una alternativa al ejemplo anterior es agrupar la lógica en funciones, y luego ejecutar condicionalmente una función o no dependiendo del valor de la variable dinámica:
function printPostProperties(int $postID)
{
$post = getPost($postID);
printPostTitle();
$addContent = isUserLoggedIn();
if ($addContent) {
printPostContent();
}
}
function printPostTitle(Post $post)
{
echo $post->title;
}
function printPostContent(Post $post)
{
echo $post->content;
}En GraphQL podemos añadir la directiva @include en la operación:
query ExportAddContent
{
addContent: isUserLoggedIn
@export(as: "addContent")
}
query PrintPostTitle($postID: ID!)
{
postWithTitle: post(by: { id: $postID }) {
title
}
}
query PrintPostContent($postID: ID!)
@depends(on: "ExportAddContent")
@include(if: $addContent)
{
postWithContent: post(by: { id: $postID }) {
content
}
}
query PrintPostProperties
@depends(on: [
"PrintPostTitle",
"PrintPostContent"
])
{
# ...
}Ahora, la operación PrintPostContent solo se ejecutará si $addContent es true.
Asignar variables, Volver a usarlas como entrada
Modifiquemos ligeramente el ejemplo anterior, en el que la condición "addContent" estaba ligada a si el usuario había iniciado sesión o no.
En este otro ejemplo, "addContent" es true siempre que hoy sea fin de semana, lo que implica algo de lógica para calcular:
- Obtener la fecha de hoy
- Formatearla al nombre del día, en minúsculas
- Comprobar que es
"saturday"o"sunday"
En PHP:
function addContent()
{
$today = time();
$dayName = date('l', $today);
$lcDayName = strtolower($dayName);
$isWeekend = in_array(
$lcDayName,
['saturday', 'sunday']
);
return $isWeekend;
}
function printPostProperties(int $postID)
{
$post = getPost($postID);
echo $post->title;
$addContent = addContent();
if ($addContent) {
echo $post->content;
}
}En GraphQL:
query ExportAddContent
{
today: _time
dayName: _date(format: "l", timestamp: $__today)
lcDayName: _strLowerCase(text: $__dayName)
isWeekend: _inArray(
value: $__lcDayName
array: ["saturday", "sunday"],
)
@export(as: "addContent")
}
query PrintPostProperties($postID: ID!)
@depends(on: "ExportAddContent")
{
post(by: { id: $postID }) {
title
content @include(if: $addContent)
}
}En la operación ExportAddContent, el valor para cada campo consultado está inmediatamente disponible para los campos debajo, bajo la variable dinámica $__fieldName. De esta forma la salida de un campo puede usarse inmediatamente como entrada de otro campo, ya dentro de la misma operación.
Esto es posible gracias a Funcionalidades personalizadas para el esquema.
Modificar dinámicamente un valor
En este ejemplo en PHP, modificamos el valor de una variable cuando el usuario logueado es un admin, en cuyo caso al contenido de la entrada se le añade un enlace para editar la entrada:
function isAdminUser()
{
$user = getCurrentUser();
return in_array("administrator", $user->roles);
}
function printPostContent(int $postID)
{
$post = getPost($postID);
$postContent = $post->content;
$isAdminUser = isAdminUser();
if ($isAdminUser) {
$postContent = sprintf(
'%s<p><a href="%s">%s</a></p>',
$postContent,
$post->edit_url,
'(Admin only) Edit post'
)
}
echo $postContent;
}En GraphQL, podemos ejecutar condicionalmente una operación u otra, produciendo valores diferentes para algún campo:
query InitializeDynamicVariables
{
isAdminUser: _echo(value: false)
@export(as: "isAdminUser")
}
query ExportConditionalVariables
@depends(on: "InitializeDynamicVariables")
{
me {
roleNames
isAdminUser: _inArray(
value: "administrator",
array: $__roleNames
)
@export(as: "isAdminUser")
}
}
query RetrieveContentForAdminUser($postId: ID!)
@depends(on: "ExportConditionalVariables")
@include(if: $isAdminUser)
{
post(by: { id : $postId }) {
originalContent: content
wpAdminEditURL
content: _sprintf(
string: "%s<p><a href=\"%s\">%s</a></p>",
values: [
$__originalContent,
$__wpAdminEditURL,
"(Admin only) Edit post"
]
)
}
}
query RetrieveContentForNonAdminUser($postId: ID!)
@depends(on: "ExportConditionalVariables")
@skip(if: $isAdminUser)
{
post(by: { id : $postId }) {
content
}
}
query ExecuteAll
@depends(on: [
"RetrieveContentForAdminUser",
"RetrieveContentForNonAdminUser"
])
{
# ...
}Al usar las directivas @include y @skip con la misma variable dinámica como entrada, las operaciones RetrieveContentForAdminUser y RetrieveContentForNonAdminUser son mutuamente exclusivas.
Iterar arrays
Digamos que queremos iterar los elementos de un array, y convertir esos valores a mayúsculas:
function printUserRolesAsUppercase(int $userID)
{
$user = getUser($userID);
foreach ($user->roles as $role) {
echo strtoupper($role);
}
}En GraphQL, podemos hacer que la directiva @underEachArrayItem itere sobre los elementos del array, y proporcione cada uno de esos valores a la siguiente directiva en la cadena, en este caso @strUpperCase:
query PrintUserRolesAsUppercase($userID: ID!)
{
user(by: { id: $userID }) {
roles
@underEachArrayItem
@strUpperCase
}
}Esto es posible gracias a las directivas componibles.
Operaciones CRUD en bulk
CRUD significa Create, Read, Update y Delete, estas son las operaciones que aplicamos sobre los recursos (entradas, usuarios, etc).
Leer en bulk en PHP tiene este aspecto:
function getPostTitles()
{
$posts = getPosts();
foreach ($posts as $post) {
echo $post->title;
}
}Este caso de uso es satisfecho naturalmente por GraphQL:
query GetPostTitles
{
posts {
title
}
}Actualizar en bulk en PHP tiene este aspecto:
function updatePostTitlesAsUppercase()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->update(['title' => strtoupper($post->title)]);
}
}Ejecutar actualizaciones en bulk en GraphQL se soporta normalmente creando una mutation dedicada updatePosts, que toma los datos para todas las entradas.
No me gusta este enfoque, ya que efectivamente duplica el número de mutations en el esquema (una para mutar el recurso individual, una para mutar múltiples recursos), y necesitamos mantener la lógica para ambas:
updatePost+updatePostscreatePost+createPosts- etc
En mi opinión, un enfoque más elegante es usar Mutaciones anidadas, donde la mutation Post.update se aplica a cada uno de los recursos consultados:
mutation UpdatePostTitlesAsUppercase
{
posts {
title
ucTitle: _strUpperCase(text: $__title)
update(
input: { title: $__ucTitle }
) {
status
post {
title
}
}
}
}El mismo enfoque funciona para borrar recursos:
function deletePosts()
{
$posts = getPosts();
foreach ($posts as $post) {
$post->delete();
}
}En GraphQL:
mutation DeletePosts
{
posts {
delete {
status
}
}
}Para la creación, no pasamos los recursos ya que aún no existen; en su lugar, proporcionamos un array con las entradas de datos para todos los recursos a crear:
function createPosts()
{
$postDataItems = [
[
'title' => 'First title',
'content' => 'First content',
],
[
'title' => 'Second title',
'content' => 'Second content',
],
];
foreach ($postDataItems as $postDataItem) {
$post = new Post($postDataItem['title'], $postDataItem['content']);
$post->save();
}
}Crear entradas en bulk en GraphQL usando una única mutation createPost es un poco peliagudo, pero no obstante factible.
La idea es iterar sobre el array con las entradas de datos, asignar cada una bajo una variable dinámica $input, y luego ejecutar la mutation createPost pasando esa entrada. Finalmente obtenemos los IDs resultantes de las entradas creadas bajo la variable dinámica $createdPostIDs, y recuperamos sus datos:
mutation CreatePosts
@depends(on: "GetPostsAndExportData")
{
createdPostIDs: _echo(value: [
{
title: "First title",
content: "First content"
},
{
title: "Second title",
content: "Second content"
},
])
@underEachArrayItem(
passValueOnwardsAs: "input"
)
@applyField(
name: "createPost"
arguments: {
input: $input
},
setResultInResponse: true
)
@export(as: "createdPostIDs")
}
query RetrieveCreatedPosts
@depends(on: "CreatePosts")
{
createdPosts: posts(
filter: {
ids: $createdPostIDs,
}
) {
title
content
}
}Enviar una petición HTTP (y otras funciones)
Enviar una petición HTTP a algún servidor web puede satisfacerse vía una función dedicada en PHP, como file_get_contents o curl_exec.
Usando file_get_contents:
$xml = file_get_contents("http://www.example.com/file.xml");En GraphQL, la lógica para ejecutar una petición HTTP puede satisfacerse vía un campo de funcionalidad, como _sendHTTPRequest:
query {
_sendHTTPRequest(input: {
url: "http://www.example.com/file.xml",
method: GET
}) {
xml: body
}
}El mismo concepto se aplica a cualquier funcionalidad.
Por ejemplo, accedemos al valor de una constante en PHP así:
$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');Podemos implementar un campo de funcionalidad correspondiente en GraphQL:
{
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}Resolver el reto usando solo GraphQL
Con todas las características de lenguaje de programación que acabamos de cubrir, ahora somos capaces de usar solo GraphQL para resolver el problema planteado anteriormente:
- Crear un webhook que sea invocado por un servicio cuando un nuevo usuario se registra en ese servicio; el usuario puede haberse suscrito al newsletter (indicado por el campo
marketing_optinen el payload del webhook); en ese caso, el webhook debe registrar el email del usuario (en el campoemailen el payload del webhook) en una lista de Mailchimp.
La solución es usar una persisted query de GraphQL como webhook, con esta consulta:
query HasSubscribedToNewsletter {
hasSubscriberOptIn: _httpRequestHasParam(name: "marketing_optin")
subscriberOptIn: _httpRequestStringParam(name: "marketing_optin")
isNotSubscriberOptInNAValue: _notEquals(value1: $__subscriberOptIn, value2: "NA")
subscribedToNewsletter: _and(values: [$__hasSubscriberOptIn, $__isNotSubscriberOptInNAValue])
@export(as: "subscribedToNewsletter")
}
query MaybeCreateContactOnMailchimp
@depends(on: "HasSubscribedToNewsletter")
@include(if: $subscribedToNewsletter)
{
subscriberEmail: _httpRequestStringParam(name: "email")
mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
mailchimpPassword: _env(name: "MAILCHIMP_API_CREDENTIALS_PASSWORD")
mailchimpListMembersJSONObject: _sendJSONObjectItemHTTPRequest(input: {
url: "https://us7.api.mailchimp.com/3.0/lists/{listCode}/members",
method: POST,
options: {
auth: {
username: $__mailchimpUsername,
password: $__mailchimpPassword
},
json: {
email_address: $__subscriberEmail,
status: "subscribed"
}
}
})
}En esta solución, la operación MaybeCreateContactOnMailchimp, que ejecuta la petición HTTP contra la API de Mailchimp, se ejecutará condicionalmente, dependiendo del valor del campo marketing_optin.
(Lee la entrada de blog 👨🏻🏫 Consulta GraphQL para enviar automáticamente los suscriptores del newsletter de InstaWP a Mailchimp para ver cómo funciona esta consulta.)
¡GraphQL es más potente de lo que pensabas!
GraphQL puede usarse para mucho más que solo obtener y mutar datos... Adaptar datos, modificar dinámicamente la salida, personalizar contenido para diferentes contextos, crear una API gateway con apenas unas pocas líneas de código, y muchos otros.
Al soportar características de lenguaje de programación, podemos resolver el reto de arriba usando solo GraphQL, y evitar desplegar un cliente que vaya junto a él. Estamos entonces simplificando el stack de la aplicación: menos partes móviles, menos complejidad, menos código a depurar, menos tecnologías con las que lidiar.
GraphQL mola 🤘