Kemampuan scripting via meta-direktif
Katakanlah kita memiliki direktif @strTitleCase yang dapat diterapkan pada field dalam query, mengubah nilainya dari "hello world!" menjadi "Hello World!", sehingga masuk akal untuk menerapkannya hanya pada field bertipe String.
Ketika menjalankan query ini:
{
post(by: { id: 1 }) {
title @strTitleCase
}
}...akan menghasilkan:
{
"data": {
"post": {
"title": "Hello World!"
}
}
}Sekarang, katakanlah tipe field-nya adalah [String] (atau [String!]), seperti pada kasus ini:
type Post {
categoryNames: [String!]
}Apa yang seharusnya terjadi ketika menerapkan direktif @strTitleCase pada field categoryNames saat menjalankan query ini?
{
post(by: { id: 1 }) {
categoryNames @strTitleCase
}
}Idealnya, respons akan berupa transformasi dari setiap nilai String di dalam array:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App"
]
}
}
}Agar hal itu terjadi, resolver direktif untuk @strTitleCase perlu memeriksa apakah inputnya adalah array, lalu melanjutkan sesuai kondisi tersebut (kode PHP ini hanyalah contoh, metode sebenarnya dalam plugin berbeda):
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);
}Itu tidak terlalu sulit. Namun, apa yang akan terjadi jika field-nya adalah array dari array String, yaitu [[String]]? Meskipun sedikit lebih sulit, direktif pun bisa menanganinya:
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);
}Lalu, bagaimana jika itu adalah [[[String]]] atau [[[[String]]]]? Implementasinya mulai menjadi sulit.
Lebih buruk lagi, boilerplate logika tambahan ini perlu diimplementasikan untuk setiap direktif yang bisa diterapkan pada array. Misalnya, untuk mengimplementasikan direktif @strUpperCase, logika ekstra ini juga akan diperlukan:
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);
}Tampaknya tidak terlalu bagus, bukan?
Solusi: memodifikasi input ke suatu direktif via direktif lain
Di sinilah penerapan direktif untuk memodifikasi perilaku direktif lain bisa terbukti berguna.
Daripada menangani setiap kemungkinan eksponen array untuk field (yaitu String, [String], [[String]], [[[String]]], dll.), @strTitleCase cukup menangani kasus dasar String:
function applyDirective(mixed $value, array $schemaDef): mixed
{
// The input will always be `String`
// Convert the String value to title case
return ucwords($value);
}Kemudian, direktif lain @underEachArrayItem dapat memodifikasi perilakunya, dengan cara:
- Mengubah satu input bertipe
[String]menjadi array input bertipeString - Mengiterasi item-item dalam array ini dan, untuk setiap item, memanggil dan menerapkan direktif downstream (
@strTitleCase), yang kemudian akan menerima input bertipeString - Mengubah kembali array nilai
Stringmenjadi satu nilai[String]
Kita kemudian dapat menjalankan query ini:
{
post(by: { id: 1 }) {
categoryNames @underEachArrayItem @strTitleCase
}
}Gif ini menunjukkan @underEachArrayItem beraksi:

Keindahan solusi ini adalah memisahkan kedalaman array dari implementasi direktif. Jika inputnya bertipe [[String]], yang perlu kita lakukan hanyalah menambahkan @underEachArrayItem tambahan, yang akan memodifikasi @underEachArrayItem yang memodifikasi direktif yang dimaksud:
{
customerAllNames @underEachArrayItem @underEachArrayItem @strTitleCase
}...menghasilkan:
{
"data": {
"customerAllNames": [
[
"John",
"Edward",
"Stevenson"
],
[
"Samantha",
"Perkins"
],
[
"Michael",
"Edward",
"Higgs"
]
]
}
}Jadi, sebagaimana dapat kita lihat, sebuah direktif yang memodifikasi direktif lain juga dapat terjadi pada pipeline direktif, di mana salah satunya memengaruhi direktif downstream, dan mereka sendiri dimodifikasi oleh direktif upstream.
Kita menyebut @underEachArrayItem sebagai "meta-direktif": sebuah direktif yang memodifikasi perilaku direktif lain. Dengan melakukan itu, ia memberikan developer kemampuan "meta-scripting", untuk menambahkan beberapa logika pemrograman di dalam query GraphQL.
Memformat query GraphQL
Karena spasi putih tidak menambah nilai semantik, kita dapat memformat query dan SDL untuk lebih mengkomunikasikan penumpukan (nesting):
{
customerAllNames
@underEachArrayItem
@underEachArrayItem
@strTitleCase
}Mendefinisikan pipeline direktif bertingkat
Bagaimana @underEachArrayItem mengetahui bahwa ia harus memodifikasi perilaku @strTitleCase? Pada contoh sebelumnya, itu karena ia ditempatkan tepat sebelumnya. Namun, apa yang seharusnya terjadi ketika kita memiliki direktif lain tepat setelah mereka?
Misalnya, dalam query ini:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@strTranslate(to: "es")
}
}...@underEachArrayItem juga harus memodifikasi perilaku direktif @strTranslate, karena direktif ini juga harus diterapkan pada String, menghasilkan respons ini:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Desarrollo web",
"Aplicación movil"
]
}
}
}Namun, direktif yang ditempatkan setelahnya juga mungkin perlu diterapkan pada array, bukan pada nilai String individual. Misalnya, direktif @arrayPad di bawah ini menambahkan entri yang hilang dalam array dengan nilai default, sehingga tidak boleh dipengaruhi oleh @underEachArrayItem:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}...menghasilkan respons ini:
{
"data": {
"post": {
"categoryNames": [
"Software",
"Web Development",
"Mobile App",
"undefined",
"undefined"
]
}
}
}Untuk membedakan antara dua situasi tersebut, kita memperkenalkan argumen affectDirectivesUnderPos pada @underEachArrayItem, yang mendefinisikan posisi relatif dari direktif-direktif yang harus dipengaruhi, sebagai array dari Int.
Dalam query di bawah ini, @underEachArrayItem mengetahui bahwa ia perlu diterapkan pada @strTitleCase dan @strTranslate, karena mereka ditempatkan pada posisi relatif 1 dan 2 dari dirinya:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
}
}Dalam query lain ini, @underEachArrayItem hanya diterapkan pada @strTitleCase (posisi relatif 1) tetapi tidak pada @arrayPad:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1])
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Nilai default untuk affectDirectivesUnderPos diatur ke [1], sehingga jika tidak ditentukan, direktif akan selalu diterapkan pada direktif tepat setelahnya. Query di atas kemudian setara dengan yang ini:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem
@strTitleCase
@arrayPad(length: 5, value: "undefined")
}
}Kita dapat mendefinisikan kombinasi direktif apa pun yang dipengaruhi oleh meta-direktif, dan yang tidak:
{
post(by: { id: 1 }) {
categoryNames
@underEachArrayItem(affectDirectivesUnderPos: [1, 2])
@strTitleCase
@strTranslate(to: "es")
@arrayPad(length: 5, value: "undefined")
}
}