Возможности скриптинга через мета-директивы
Предположим, у нас есть директива @strTitleCase, которую можно применить к полю в запросе, преобразуя его значение с "hello world!" на "Hello World!", поэтому логично применять её только к полям типа String.
При выполнении этого запроса:
{
post(by: { id: 1 }) {
title @strTitleCase
}
}...будет получен результат:
{
"data": {
"post": {
"title": "Hello World!"
}
}
}Теперь предположим, что тип поля — [String] (или [String!]), как в этом случае:
type Post {
categoryNames: [String!]
}Что должно произойти при применении директивы @strTitleCase к полю categoryNames при выполнении этого запроса?
{
post(by: { id: 1 }) {
categoryNames @strTitleCase
}
}В идеале ответ должен представлять собой преобразование каждого значения String внутри массива:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App"
]
}
}
}Чтобы это произошло, резолвер директивы @strTitleCase должен будет проверять, является ли входное значение массивом, и действовать соответственно (этот PHP-код приведён в качестве примера, реальный метод в плагине отличается):
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);
}Это не слишком сложно. Но что произойдёт, если поле является массивом массивов String, то есть [[String]]? Хотя это несколько сложнее, директива также может с этим справиться:
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);
}А что, если это [[[String]]] или [[[[String]]]]? Реализовать это становится всё сложнее.
Что ещё хуже, этот дополнительный шаблонный код пришлось бы реализовывать для любой директивы, которая может применяться к массивам. Например, для реализации директивы @strUpperCase также потребуется эта дополнительная логика:
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);
}Выглядит не очень красиво, не правда ли?
Решение: изменение входных данных директивы через другую директиву
Именно здесь применение директивы для изменения поведения другой директивы оказывается полезным.
Вместо того чтобы обрабатывать каждую возможную вложенность массивов для поля (то есть String, [String], [[String]], [[[String]]] и т. д.), @strTitleCase может просто работать с базовым случаем String:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// The input will always be `String`
// Convert the String value to title case
return ucwords($value);
}Затем другая директива @underEachArrayItem может изменить её поведение, выполняя следующее:
- Преобразование единственного входного значения типа
[String]в массив входных значений типаString - Перебор элементов этого массива и для каждого — вызов и применение нижестоящей директивы (
@strTitleCase), которая получит входное значение типаString - Преобразование массива значений
Stringобратно в единственное значение[String]
Затем можно выполнить этот запрос:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}Этот gif показывает @underEachArrayItem в действии:

Красота этого решения заключается в том, что оно отделяет глубину массива от реализации директивы. Если входное значение имеет тип [[String]], всё что нужно сделать — добавить дополнительный @underEachArrayItem, который будет изменять @underEachArrayItem, изменяющий целевую директиву:
{
customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}...что приведёт к следующему результату:
{
"data": {
"customerAllNames": [
[
"John",
"Edward",
"Stevenson"
],
[
"Samantha",
"Perkins"
],
[
"Michael",
"Edward",
"Higgs"
]
]
}
}Таким образом, как мы можем видеть, директива, изменяющая другую директиву, также может существовать в конвейере директив, где одна из них влияет на нижестоящую директиву, а сами они изменяются вышестоящей директивой.
Мы называем @underEachArrayItem «мета-директивой»: директивой, которая изменяет поведение другой директивы. Тем самым она предоставляет разработчику возможности «мета-скриптинга» — добавления программной логики непосредственно внутри GraphQL-запроса.
Форматирование GraphQL-запроса
Поскольку пробелы не несут семантической нагрузки, можно отформатировать запрос и SDL, чтобы лучше передать вложенность:
{
customerAllNames
@underEachArrayItem
@underEachArrayItem
@strTitleCase
}Определение конвейера вложенных директив
Как @underEachArrayItem узнаёт, что должна изменять поведение @strTitleCase? В предыдущем примере — потому что она была расположена прямо перед ней. Но что должно происходить, когда сразу после них стоит ещё одна директива?
Например, в этом запросе:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@strTranslate(to: "es")
}
}...@underEachArrayItem должна также изменять поведение директивы @strTranslate, поскольку эта директива тоже должна применяться к String, что даёт следующий ответ:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Desarrollo web",
"Aplicación movil"
]
}
}
}Однако директива, расположенная после, также может применяться к массиву, а не к отдельному значению String. Например, директива @arrayPad ниже добавляет отсутствующие записи в массив со значениями по умолчанию, поэтому она не должна быть затронута @underEachArrayItem:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}...что даёт следующий ответ:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App",
"undefined",
"undefined"
]
}
}
}Чтобы различить эти две ситуации, мы вводим аргумент affectDirectivesUnderPos в @underEachArrayItem, который определяет относительные позиции директив, на которые нужно воздействовать, в виде массива Int.
В запросе ниже @underEachArrayItem знает, что должна применяться к @strTitleCase и @strTranslate, поскольку они расположены на относительных позициях 1 и 2 от неё:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
}
}В этом другом запросе @underEachArrayItem применяется только к @strTitleCase (относительная позиция 1), но не к @arrayPad:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1])
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Значение по умолчанию для affectDirectivesUnderPos равно [1], поэтому если оно не указано, директива всегда будет применяться к директиве, стоящей непосредственно после неё. Приведённый выше запрос эквивалентен следующему:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Можно определить любую комбинацию директив, на которые воздействует мета-директива, и директив, на которые она не воздействует:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
@arrayPad(length: 5, value: "undefined")
}
}