Capacità di scripting tramite le meta-direttive
Supponiamo di avere una direttiva @strTitleCase che può essere applicata sul campo nella query, trasformando il suo valore da "hello world!" a "Hello World!", quindi ha senso applicarla solo su campi di tipo String.
Eseguendo questa query:
{
post(by: { id: 1 }) {
title @strTitleCase
}
}...produrrà:
{
"data": {
"post": {
"title": "Hello World!"
}
}
}Ora, supponiamo che il tipo del campo sia [String] (o [String!]), come in questo caso:
type Post {
categoryNames: [String!]
}Cosa dovrebbe accadere quando si applica la direttiva @strTitleCase sul campo categoryNames eseguendo questa query?
{
post(by: { id: 1 }) {
categoryNames @strTitleCase
}
}Idealmente, la risposta sarà una trasformazione di ogni valore String all'interno dell'array:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App"
]
}
}
}Affinché ciò avvenga, il resolver della direttiva @strTitleCase dovrà verificare se l'input è un array e procedere di conseguenza (questo codice PHP è un esempio, il metodo reale nel plugin è diverso):
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);
}Non è molto difficile. Ma allora, cosa accadrebbe se il campo fosse un array di array di String, cioè [[String]]? Anche se un po' più difficile, la direttiva può gestire anche questo:
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);
}E poi, cosa fare se è un [[[String]]] o [[[[String]]]]? Inizia a diventare difficile da implementare.
Peggio ancora, questo codice boilerplate aggiuntivo dovrebbe essere implementato per qualsiasi direttiva che potrebbe essere applicata sugli array. Ad esempio, per implementare una direttiva @strUpperCase, sarà necessaria anche questa logica 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);
}Non sembra molto elegante, vero?
Soluzione: modificare l'input di una direttiva tramite un'altra direttiva
È qui che applicare una direttiva per modificare il comportamento di un'altra direttiva può rivelarsi utile.
Invece di trattare ogni possibile esponente di array per il campo (cioè String, [String], [[String]], [[[String]]], ecc.), @strTitleCase può semplicemente trattare il 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);
}E poi, un'altra direttiva @underEachArrayItem può modificarne il comportamento, tramite:
- La conversione dell'input singolo di tipo
[String]in un array di input di tipoString - L'iterazione degli elementi di questo array e, per ciascuno, l'invocazione e l'applicazione della direttiva a valle (
@strTitleCase), che riceverà quindi un input di tipoString - La riconversione dell'array di valori
Stringin un singolo valore[String]
Possiamo quindi eseguire questa query:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}Questa gif mostra @underEachArrayItem in azione:

La bellezza di questa soluzione è che disaccoppia la profondità dell'array dall'implementazione della direttiva. Se l'input è di tipo [[String]], tutto ciò che dobbiamo fare è aggiungere un @underEachArrayItem aggiuntivo, che modificherà l'@underEachArrayItem che modifica la direttiva desiderata:
{
customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}...producendo:
{
"data": {
"customerAllNames": [
[
"John",
"Edward",
"Stevenson"
],
[
"Samantha",
"Perkins"
],
[
"Michael",
"Edward",
"Higgs"
]
]
}
}Quindi, come possiamo apprezzare, una direttiva che modifica una direttiva può verificarsi anche in una pipeline di direttive, dove una di esse influenza una direttiva a valle, e dove esse stesse sono modificate da una direttiva a monte.
Chiamiamo @underEachArrayItem una "meta-direttiva": una direttiva che modifica il comportamento di un'altra direttiva. Così facendo, conferisce allo sviluppatore capacità di "meta-scripting", per aggiungere logica di programmazione all'interno della query GraphQL.
Formattazione della query GraphQL
Dato che gli spazi bianchi non aggiungono valore semantico, possiamo formattare la query e l'SDL per esprimere meglio l'annidamento:
{
customerAllNames
@underEachArrayItem
@underEachArrayItem
@strTitleCase
}Definire una pipeline di direttive annidate
Come fa @underEachArrayItem a sapere che deve modificare il comportamento di @strTitleCase? Nell'esempio precedente, era perché era posizionata subito prima di essa. Ma cosa dovrebbe accadere quando abbiamo ancora un'altra direttiva subito dopo di esse?
Ad esempio, in questa query:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@strTranslate(to: "es")
}
}...@underEachArrayItem dovrebbe modificare anche il comportamento della direttiva @strTranslate, poiché anche questa direttiva deve essere applicata a un String, producendo questa risposta:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Desarrollo web",
"Aplicación movil"
]
}
}
}Tuttavia, una direttiva posizionata successivamente potrebbe anche aver bisogno di essere applicata all'array, e non al singolo valore String. Ad esempio, la direttiva @arrayPad qui sotto aggiunge le voci mancanti in un array con valori predefiniti, quindi non dovrebbe essere influenzata da @underEachArrayItem:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}...producendo questa risposta:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App",
"undefined",
"undefined"
]
}
}
}Per distinguere tra le due situazioni, introduciamo l'argomento affectDirectivesUnderPos in @underEachArrayItem, che definisce la posizione relativa delle direttive che devono essere influenzate, sotto forma di array di Int.
Nella query qui sotto, @underEachArrayItem sa che deve essere applicata a @strTitleCase e @strTranslate, poiché sono posizionate alle posizioni relative 1 e 2 rispetto a essa:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
}
}In quest'altra query, @underEachArrayItem è applicata solo a @strTitleCase (posizione relativa 1) ma non a @arrayPad:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1])
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Il valore predefinito di affectDirectivesUnderPos è [1], quindi se non specificato, la direttiva sarà sempre applicata alla direttiva che la segue immediatamente. La query qui sopra è quindi equivalente a questa:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Possiamo definire qualsiasi combinazione di direttive influenzate dalla meta-direttiva, e altre no:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
@arrayPad(length: 5, value: "undefined")
}
}