Conceitos, Ideias, Estratégias
Conceitos, Ideias, EstratégiasCapacidades de scripting via meta-diretivas

Capacidades de scripting via meta-diretivas

Digamos que temos uma diretiva @strTitleCase que pode ser aplicada em um campo na query, transformando seu valor de "hello world!" para "Hello World!", portanto faz sentido aplicá-la apenas em campos do tipo String.

Ao executar esta query:

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

...ela produzirá:

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

Agora, digamos que o tipo do campo seja [String] (ou [String!]), como neste caso:

type Post {
  categoryNames: [String!]
}

O que deveria acontecer ao aplicar a diretiva @strTitleCase no campo categoryNames ao executar esta query?

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

Idealmente, a resposta será uma transformação de cada valor String dentro do array:

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

Para que isso aconteça, o resolver da diretiva @strTitleCase precisará verificar se o input é um array e proceder adequadamente (este código PHP é um exemplo; o método real no plugin é diferente):

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

Não é muito difícil. Mas então, o que aconteceria se o campo fosse um array de array de String, ou seja, [[String]]? Embora um pouco mais difícil, a diretiva também consegue lidar com isso:

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 então, e se for um [[[String]]] ou [[[[String]]]]? Começa a ficar difícil de implementar.

Pior ainda, esse boilerplate de lógica adicional precisaria ser implementado para qualquer diretiva que pudesse ser aplicada em arrays. Por exemplo, para implementar uma diretiva @strUpperCase, essa lógica extra também será necessária:

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

Não parece muito elegante, certo?

Solução: modificar o input de uma diretiva via outra diretiva

É aqui que aplicar uma diretiva para modificar o comportamento de outra diretiva pode ser útil.

Em vez de lidar com cada possível expoente de arrays para o campo (ou seja, String, [String], [[String]], [[[String]]], etc.), @strTitleCase pode simplesmente tratar o 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 então, outra diretiva @underEachArrayItem pode modificar seu comportamento, ao:

  1. Converter o input único do tipo [String] em um array de inputs do tipo String
  2. Iterar os itens nesse array e, para cada um, invocar e aplicar a diretiva downstream (@strTitleCase), que receberá então um input do tipo String
  3. Converter de volta o array de valores String em um único valor [String]

Podemos então executar esta query:

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

Este gif mostra @underEachArrayItem em ação:

Adicionando @underEachArrayItem para modificar outra diretiva

A beleza desta solução é que ela desacopla a profundidade do array da implementação da diretiva. Se o input for do tipo [[String]], tudo que precisamos fazer é adicionar um @underEachArrayItem adicional, que modificará o @underEachArrayItem que modifica a diretiva desejada:

{
  customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}

...produzindo:

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

Portanto, como podemos perceber, uma diretiva modificando outra diretiva também pode ocorrer em uma pipeline de diretivas, onde uma delas afeta uma diretiva downstream, e elas próprias são modificadas por uma diretiva upstream.

Chamamos @underEachArrayItem de "meta-diretiva": uma diretiva que modifica o comportamento de outra diretiva. Ao fazer isso, ela oferece ao desenvolvedor capacidades de "meta-scripting", para adicionar alguma lógica de programação dentro da query GraphQL.

Formatando a query GraphQL

Como espaços em branco não adicionam valor semântico, podemos formatar a query e o SDL para transmitir melhor o aninhamento:

{
  customerAllNames
    @underEachArrayItem
      @underEachArrayItem
        @strTitleCase
}

Definindo uma pipeline de diretivas aninhadas

Como @underEachArrayItem sabe que deve modificar o comportamento de @strTitleCase? No exemplo anterior, era porque estava posicionada logo antes dela. Mas o que deveria acontecer quando temos ainda outra diretiva logo após elas?

Por exemplo, nesta query:

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

...@underEachArrayItem também deveria modificar o comportamento da diretiva @strTranslate, já que essa diretiva também deve ser aplicada a um String, produzindo esta resposta:

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

No entanto, uma diretiva posicionada depois também pode precisar ser aplicada ao array, e não ao valor String individual. Por exemplo, a diretiva @arrayPad abaixo adiciona entradas ausentes em um array com valores padrão, portanto não deveria ser afetada por @underEachArrayItem:

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

...produzindo esta resposta:

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

Para distinguir entre as duas situações, introduzimos o argumento affectDirectivesUnderPos em @underEachArrayItem, que define a posição relativa das diretivas que devem ser afetadas, como um array de Int.

Na query abaixo, @underEachArrayItem sabe que precisa ser aplicada a @strTitleCase e @strTranslate, pois estão nas posições relativas 1 e 2 a partir dela:

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

Nesta outra query, @underEachArrayItem é aplicada apenas a @strTitleCase (posição relativa 1) mas não a @arrayPad:

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

O valor padrão de affectDirectivesUnderPos é [1], portanto, se não especificado, a diretiva sempre será aplicada à diretiva logo após ela. A query acima é então equivalente a esta:

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

Podemos definir qualquer combinação de diretivas afetadas pela meta-diretiva, e outras não:

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