Koncepcje, Idee, Strategie
Koncepcje, Idee, StrategieMożliwości skryptowania via meta-dyrektywy

Możliwości skryptowania via meta-dyrektywy

Powiedzmy, że mamy dyrektywę @strTitleCase, która może być stosowana na polu w query, przekształcając jego wartość z "hello world!" na "Hello World!", dlatego sensowne jest stosowanie jej tylko na polach typu String.

Przy wykonaniu tej query:

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

...zostanie wyprodukowane:

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

Teraz powiedzmy, że typ pola to [String] (lub [String!]), jak w tym przypadku:

type Post {
  categoryNames: [String!]
}

Co powinno się stać przy stosowaniu dyrektywy @strTitleCase na polu categoryNames podczas wykonywania tej query?

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

Idealnie odpowiedź będzie transformacją każdej wartości String wewnątrz tablicy:

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

Aby to osiągnąć, resolver dyrektywy @strTitleCase będzie musiał sprawdzić, czy dane wejściowe są tablicą i odpowiednio postąpić (ten kod PHP jest przykładem; rzeczywista metoda w pluginie jest inna):

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

To nie jest zbyt trudne. Ale co by się stało, gdyby pole było tablicą tablic String, czyli [[String]]? Choć nieco trudniej, dyrektywa może sobie z tym poradzić:

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

A co, jeśli byłoby to [[[String]]] lub [[[[String]]]]? Zaczyna to być trudne do zaimplementowania.

Co gorsza, ten dodatkowy boilerplate logiki musiałby być zaimplementowany dla każdej dyrektywy, która mogłaby być stosowana na tablicach. Na przykład, aby zaimplementować dyrektywę @strUpperCase, ta dodatkowa logika również będzie wymagana:

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

Nie wygląda to zbyt elegancko, prawda?

Rozwiązanie: modyfikowanie danych wejściowych dyrektywy za pomocą innej dyrektywy

Tu właśnie stosowanie dyrektywy w celu modyfikacji zachowania innej dyrektywy może okazać się przydatne.

Zamiast obsługiwać każdy możliwy wykładnik tablic dla pola (tj. String, [String], [[String]], [[[String]]], itp.), @strTitleCase może po prostu obsługiwać przypadek bazowy String:

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

Następnie inna dyrektywa @underEachArrayItem może modyfikować jej zachowanie, poprzez:

  1. Konwersję pojedynczego wejścia typu [String] na tablicę wejść typu String
  2. Iterację elementów tej tablicy i dla każdego wywołanie i zastosowanie dyrektywy downstream (@strTitleCase), która otrzyma wówczas wejście typu String
  3. Konwersję z powrotem tablicy wartości String na pojedynczą wartość [String]

Możemy wtedy wykonać tę query:

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

Ten gif pokazuje @underEachArrayItem w akcji:

Dodawanie @underEachArrayItem do modyfikacji innej dyrektywy

Piękno tego rozwiązania polega na tym, że oddziela głębokość tablicy od implementacji dyrektywy. Jeśli wejście jest typu [[String]], wystarczy dodać dodatkowe @underEachArrayItem, które zmodyfikuje @underEachArrayItem modyfikujące zamierzoną dyrektywę:

{
  customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}

...produkując:

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

Jak więc możemy zaobserwować, dyrektywa modyfikująca dyrektywę może również wystąpić w potoku dyrektyw, gdzie jedna z nich wpływa na dyrektywę downstream, a same są modyfikowane przez dyrektywę upstream.

Nazywamy @underEachArrayItem "meta-dyrektywą": dyrektywą, która modyfikuje zachowanie innej dyrektywy. Czyniąc to, daje deweloperowi możliwości "meta-skryptowania", aby dodać pewną logikę programistyczną wewnątrz query GraphQL.

Formatowanie query GraphQL

Ponieważ białe znaki nie dodają wartości semantycznej, możemy sformatować query i SDL, aby lepiej przekazać zagnieżdżenie:

{
  customerAllNames
    @underEachArrayItem
      @underEachArrayItem
        @strTitleCase
}

Definiowanie potoku zagnieżdżonych dyrektyw

Skąd @underEachArrayItem wie, że musi modyfikować zachowanie @strTitleCase? W poprzednim przykładzie było to dlatego, że była umieszczona bezpośrednio przed nią. Ale co powinno się stać, gdy mamy jeszcze inną dyrektywę zaraz po nich?

Na przykład w tej query:

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

...@underEachArrayItem powinna również modyfikować zachowanie dyrektywy @strTranslate, ponieważ ta dyrektywa musi być również stosowana do String, produkując tę odpowiedź:

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

Jednakże dyrektywa umieszczona po nich może również wymagać zastosowania do tablicy, a nie do indywidualnej wartości String. Na przykład dyrektywa @arrayPad poniżej dodaje brakujące wpisy w tablicy z wartościami domyślnymi, dlatego nie powinna być dotknięta przez @underEachArrayItem:

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

...produkując tę odpowiedź:

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

Aby rozróżnić między dwoma sytuacjami, wprowadzamy argument affectDirectivesUnderPos do @underEachArrayItem, który definiuje względną pozycję dyrektyw, które mają być dotknięte, jako tablicę Int.

W poniższej query @underEachArrayItem wie, że musi być zastosowana do @strTitleCase i @strTranslate, ponieważ są umieszczone na względnych pozycjach 1 i 2 od niej:

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

W tej innej query @underEachArrayItem jest stosowana tylko do @strTitleCase (względna pozycja 1), ale nie do @arrayPad:

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

Domyślna wartość affectDirectivesUnderPos wynosi [1], więc jeśli nie zostanie określona, dyrektywa zawsze będzie stosowana do dyrektywy bezpośrednio po niej. Powyższa query jest wtedy równoważna tej:

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

Możemy zdefiniować dowolną kombinację dyrektyw dotkniętych przez meta-dyrektywę i innych, które nie są:

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