Concepten, Ideeën, Strategieën
Concepten, Ideeën, StrategieënScriptingmogelijkheden via meta-directives

Scriptingmogelijkheden via meta-directives

Stel dat we een directive @strTitleCase hebben die op een veld in de query kan worden toegepast en de waarde ervan transformeert van "hello world!" naar "Hello World!", zodat het logisch is om deze alleen toe te passen op velden van het type String.

Bij het uitvoeren van deze query:

{
  post(by: { id: 1 }) {
    title @strTitleCase
  }
}

...wordt het volgende geproduceerd:

{
  "data": {
    "post": {
      "title": "Hello World!"
    }
  }
}

Stel nu dat het veldtype [String] is (of [String!]), zoals in dit geval:

type Post {
  categoryNames: [String!]
}

Wat zou er moeten gebeuren als je directive @strTitleCase toepast op het veld categoryNames bij het uitvoeren van deze query?

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

Idealiter is het antwoord een transformatie van elke String-waarde binnen de array:

{
  "data": {
    "post": {
      "categoryNames": [
        "Software",
        "Web Development",
        "Mobile App"
      ]
    }
  }
}

Om dat te bereiken, moet de directive resolver voor @strTitleCase controleren of de invoer een array is en dienovereenkomstig handelen (deze PHP-code is een voorbeeld; de werkelijke methode in de plugin is anders):

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

Dat is niet zo moeilijk. Maar wat zou er gebeuren als het veld een array van arrays van String is, d.w.z. [[String]]? Hoewel wat moeilijker, kan de directive daar ook mee omgaan:

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

En wat als het [[[String]]] of [[[[String]]]] is? Dan wordt het steeds moeilijker om te implementeren.

Erger nog, deze extra boilerplate-logica zou geïmplementeerd moeten worden voor elke directive die op arrays kan worden toegepast. Om bijvoorbeeld een directive @strUpperCase te implementeren, is deze extra logica ook vereist:

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

Dat ziet er niet erg fraai uit, toch?

Oplossing: de invoer van een directive wijzigen via een andere directive

Dit is precies waar het toepassen van een directive om het gedrag van een andere directive te wijzigen nuttig kan zijn.

In plaats van elk mogelijk exponentieel niveau van arrays voor het veld te behandelen (d.w.z. String, [String], [[String]], [[[String]]], enzovoort), kan @strTitleCase gewoon het basisgeval String afhandelen:

function applyDirective(mixed $value, array $schemaDef): mixed
{
  // The input will always be `String`
  // Convert the String value to title case
  return ucwords($value);
}

Vervolgens kan een andere directive @underEachArrayItem het gedrag ervan wijzigen door:

  1. De enkele invoer van het type [String] om te zetten in een array van invoerwaarden van het type String
  2. De items in deze array te itereren en voor elk de downstream directive (@strTitleCase) aan te roepen en toe te passen, die dan een invoer van het type String ontvangt
  3. De array van String-waarden terug om te zetten naar één enkele [String]-waarde

We kunnen dan deze query uitvoeren:

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

Deze gif toont @underEachArrayItem in actie:

Het toevoegen van @underEachArrayItem om een andere directive te wijzigen

Het mooie van deze oplossing is dat de diepte van de array losgekoppeld wordt van de implementatie van de directive. Als de invoer van het type [[String]] is, hoeven we alleen een extra @underEachArrayItem toe te voegen, die de @underEachArrayItem wijzigt die de beoogde directive wijzigt:

{
  customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}

...wat dit produceert:

{
  "data": {
    "customerAllNames": [
      [
        "John",
        "Edward",
        "Stevenson"
      ],
      [
        "Samantha",
        "Perkins"
      ],
      [
        "Michael",
        "Edward",
        "Higgs"
      ]
    ]
  }
}

Zoals je kunt zien, kan een directive die een andere directive wijzigt ook voorkomen in een pipeline van directives, waarbij één ervan een downstream directive beïnvloedt en ze zelf worden gewijzigd door een upstream directive.

We noemen @underEachArrayItem een "meta-directive": een directive die het gedrag van een andere directive wijzigt. Daarmee geeft het de ontwikkelaar "meta-scripting"-mogelijkheden om wat programmalogica toe te voegen binnen de GraphQL-query.

De GraphQL-query opmaken

Omdat witruimte geen semantische waarde toevoegt, kunnen we de query en SDL opmaken om de nesting beter weer te geven:

{
  customerAllNames
    @underEachArrayItem
      @underEachArrayItem
        @strTitleCase
}

Een pipeline van geneste directives definiëren

Hoe weet @underEachArrayItem dat het het gedrag van @strTitleCase moet wijzigen? In het vorige voorbeeld was dat omdat het er direct voor geplaatst was. Maar wat zou er moeten gebeuren als er nog een andere directive direct na hen staat?

Bijvoorbeeld in deze query:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem
        @strTitleCase
        @strTranslate(to: "es")
  }
}

...zou @underEachArrayItem ook het gedrag van directive @strTranslate moeten wijzigen, omdat deze directive ook op een String moet worden toegepast, wat dit antwoord oplevert:

{
  "data": {
    "post": {
      "categoryNames": [
        "Software",
        "Desarrollo web",
        "Aplicación movil"
      ]
    }
  }
}

Een directive die erna geplaatst wordt, kan echter ook op de array moeten worden toegepast en niet op de individuele String-waarde. Zo voegt directive @arrayPad hieronder ontbrekende items toe aan een array met standaardwaarden, en zou het dus niet beïnvloed moeten worden door @underEachArrayItem:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem
        @strTitleCase
      @arrayPad(length: 5, value: "undefined")
  }
}

...wat dit antwoord oplevert:

{
  "data": {
    "post": {
      "categoryNames": [
        "Software",
        "Web Development",
        "Mobile App",
        "undefined",
        "undefined"
      ]
    }
  }
}

Om onderscheid te maken tussen de twee situaties, introduceren we het argument affectDirectivesUnderPos voor @underEachArrayItem, dat de relatieve positie definieert van de directives die beïnvloed moeten worden, als een array van Int.

In de query hieronder weet @underEachArrayItem dat het op @strTitleCase en @strTranslate moet worden toegepast, omdat die op de relatieve posities 1 en 2 van zichzelf staan:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem(affectDirectivesUnderPos: [1, 2])
        @strTitleCase
        @strTranslate(to: "es")
  }
}

In deze andere query wordt @underEachArrayItem alleen toegepast op @strTitleCase (relatieve positie 1), maar niet op @arrayPad:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem(affectDirectivesUnderPos: [1])
        @strTitleCase
      @arrayPad(length: 5, value: "undefined")
  }
}

De standaardwaarde van affectDirectivesUnderPos is ingesteld op [1], dus als deze niet opgegeven wordt, wordt de directive altijd toegepast op de directive direct erna. De bovenstaande query is dan equivalent aan deze:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem
        @strTitleCase
      @arrayPad(length: 5, value: "undefined")
  }
}

We kunnen elke combinatie definiëren van directives die wel en niet beïnvloed worden door de meta-directive:

{
  post(by: { id: 1 }) {
    categoryNames
      @underEachArrayItem(affectDirectivesUnderPos: [1, 2])
        @strTitleCase
        @strTranslate(to: "es")
      @arrayPad(length: 5, value: "undefined")
  }
}