Capacidades de scripting mediante meta-directivas
Supongamos que tenemos una directiva @strTitleCase que se puede aplicar sobre el campo en la consulta, transformando su valor de "hello world!" a "Hello World!", por lo que tiene sentido aplicarla únicamente sobre campos de tipo String.
Al ejecutar esta consulta:
{
post(by: { id: 1 }) {
title @strTitleCase
}
}...producirá:
{
"data": {
"post": {
"title": "Hello World!"
}
}
}Ahora, supongamos que el tipo de campo es [String] (o [String!]), como en este caso:
type Post {
categoryNames: [String!]
}¿Qué debería ocurrir al aplicar la directiva @strTitleCase sobre el campo categoryNames al ejecutar esta consulta?
{
post(by: { id: 1 }) {
categoryNames @strTitleCase
}
}Lo ideal sería que la respuesta fuera una transformación de cada valor String dentro del array:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App"
]
}
}
}Para que eso ocurra, el resolver de la directiva @strTitleCase deberá comprobar si la entrada es un array y proceder en consecuencia (este código PHP es un ejemplo, el método real en el plugin es distinto):
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array to title case
if ($schemaDef['isArray']) {
return array_map(ucwords(...), $value);
}
// Convert the String value to title case
return ucwords($value);
}Eso no es muy difícil. Pero entonces, ¿qué ocurriría si el campo es un array de array de String, es decir, [[String]]? Aunque algo más difícil, la directiva también puede manejarlo:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array of arrays to title case
if ($schemaDef['isArrayOfArrays']) {
return array_map(
fn (array $array) => array_map(ucwords(...), $array),
$value
);
}
// Convert each item in an array to title case
if ($schemaDef['isArray']) {
return array_map(ucwords(...), $value);
}
// Convert the String value to title case
return ucwords($value);
}Y entonces, ¿qué pasa si es un [[[String]]] o [[[[String]]]]? Empieza a resultar difícil de implementar.
Peor aún, este boilerplate adicional habría que implementarlo para cualquier directiva que pudiera aplicarse sobre arrays. Por ejemplo, para implementar una directiva @strUpperCase, también será necesaria esta lógica extra:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// Convert each item in an array of arrays to uppercase
if ($schemaDef['isArrayOfArrays']) {
return array_map(
fn (array $array) => array_map(strtoupper(...), $array),
$value
);
}
// Convert each item in an array to uppercase
if ($schemaDef['isArray']) {
return array_map(strtoupper(...), $value);
}
// Convert the String value to uppercase
return strtoupper($value);
}No queda muy bonito, ¿verdad?
Solución: modificar la entrada a una directiva mediante otra directiva
Aquí es donde aplicar una directiva para modificar el comportamiento de otra directiva puede resultar útil.
En lugar de tratar con cada posible exponente de arrays para el campo (es decir, String, [String], [[String]], [[[String]]], etc.), @strTitleCase puede tratar únicamente el caso base String:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// The input will always be `String`
// Convert the String value to title case
return ucwords($value);
}Y entonces, otra directiva @underEachArrayItem puede modificar su comportamiento, mediante:
- Convertir la única entrada de tipo
[String]en un array de entradas de tipoString - Iterar los elementos de este array y, para cada uno, invocar y aplicar la directiva siguiente (
@strTitleCase), que entonces recibirá una entrada de tipoString - Convertir de nuevo el array de valores
Stringen un único valor[String]
Podemos entonces ejecutar esta consulta:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}Este gif muestra @underEachArrayItem en acción:

La belleza de esta solución es que desacopla la profundidad del array de la implementación de la directiva. Si la entrada es de tipo [[String]], solo necesitamos añadir un @underEachArrayItem adicional, que modificará al @underEachArrayItem que modifica a la directiva deseada:
{
customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}...produciendo:
{
"data": {
"customerAllNames": [
[
"John",
"Edward",
"Stevenson"
],
[
"Samantha",
"Perkins"
],
[
"Michael",
"Edward",
"Higgs"
]
]
}
}Así pues, como podemos apreciar, una directiva que modifica a una directiva también puede darse en una pipeline de directivas, donde una de ellas afecta a una directiva siguiente, y a su vez son modificadas por una directiva previa.
Llamamos a @underEachArrayItem una "meta-directiva": una directiva que modifica el comportamiento de otra directiva. Al hacerlo, está dando al desarrollador capacidades de "meta-scripting", para añadir algo de lógica de programación dentro de la consulta GraphQL.
Dando formato a la consulta GraphQL
Dado que los espacios en blanco no añaden valor semántico, podemos dar formato a la consulta y al SDL para transmitir mejor el anidamiento:
{
customerAllNames
@underEachArrayItem
@underEachArrayItem
@strTitleCase
}Definiendo una pipeline de directivas anidadas
¿Cómo sabe @underEachArrayItem que debe modificar el comportamiento de @strTitleCase? En el ejemplo anterior, fue porque estaba colocada justo antes. Pero ¿qué debería ocurrir cuando tenemos otra directiva más justo después?
Por ejemplo, en esta consulta:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@strTranslate(to: "es")
}
}...@underEachArrayItem también debería modificar el comportamiento de la directiva @strTranslate, ya que esta directiva también debe aplicarse sobre un String, produciendo esta respuesta:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Desarrollo web",
"Aplicación movil"
]
}
}
}Sin embargo, una directiva colocada después también podría necesitar aplicarse sobre el array, y no sobre el valor individual String. Por ejemplo, la directiva @arrayPad de abajo añade entradas que faltan en un array con valores por defecto, por lo que no debe verse afectada por @underEachArrayItem:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}...produciendo esta respuesta:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App",
"undefined",
"undefined"
]
}
}
}Para distinguir entre las dos situaciones, introducimos el argumento affectDirectivesUnderPos en @underEachArrayItem, que define la posición relativa de las directivas que deben verse afectadas, como un array de Int.
En la consulta de abajo, @underEachArrayItem sabe que debe aplicarse sobre @strTitleCase y @strTranslate, ya que están colocadas en las posiciones relativas 1 y 2 respecto a ella:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
}
}En esta otra consulta, @underEachArrayItem se aplica solo sobre @strTitleCase (posición relativa 1) pero no sobre @arrayPad:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1])
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}El valor por defecto de affectDirectivesUnderPos es [1], por lo que si no se especifica, la directiva siempre se aplicará a la directiva inmediatamente posterior. La consulta de arriba es entonces equivalente a esta:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Podemos definir cualquier combinación de directivas afectadas por la meta-directiva, y otras no:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
@arrayPad(length: 5, value: "undefined")
}
}