Concetti, idee, strategie
Concetti, idee, strategieCapacità di scripting tramite le meta-direttive

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:

  1. La conversione dell'input singolo di tipo [String] in un array di input di tipo String
  2. 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 tipo String
  3. La riconversione dell'array di valori String in un singolo valore [String]

Possiamo quindi eseguire questa query:

{
  post(by: { id: 1 }) {
    categoryNames @underEachArrayItem @strTitleCase
  }
}

Questa gif mostra @underEachArrayItem in azione:

Aggiunta di @underEachArrayItem per modificare un'altra direttiva

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")
  }
}