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:
- Konwersję pojedynczego wejścia typu
[String]na tablicę wejść typuString - Iterację elementów tej tablicy i dla każdego wywołanie i zastosowanie dyrektywy downstream (
@strTitleCase), która otrzyma wówczas wejście typuString - Konwersję z powrotem tablicy wartości
Stringna pojedynczą wartość[String]
Możemy wtedy wykonać tę query:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}Ten gif pokazuje @underEachArrayItem w akcji:

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