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:
- Converter o input único do tipo
[String]em um array de inputs do tipoString - Iterar os itens nesse array e, para cada um, invocar e aplicar a diretiva downstream (
@strTitleCase), que receberá então um input do tipoString - Converter de volta o array de valores
Stringem um único valor[String]
Podemos então executar esta query:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}Este gif mostra @underEachArrayItem em ação:

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