Blog

๐Ÿ•ธ Bagaimana dan Di Mana GraphQL Dapat Meningkatkan WordPress, Melengkapi REST API

Leonardo Losoviz
Oleh Leonardo Losoviz ยท

Pembaruan 01/05/2024: Lihat perbandingan Gato GraphQL vs WP REST API.

Akhir pekan lalu saya menerbitkan posting blog ๐Ÿฆธ๐Ÿฟโ€โ™‚๏ธ Gato GraphQL kini ditranspilasi dari PHP 8.0 ke 7.1.

Setelah membagikan posting tersebut di Reddit /r/php, komunitas memulai diskusi yang hidup tentang seberapa bermanfaatnya menggunakan GraphQL di WordPress, seberapa berbedanya dari WP REST API, dan seberapa beralasannya membawa API lain ke WordPress.

Saya pikir sebagian besar komentar tepat sasaran, dan sebagian lainnya melewatkan beberapa informasi penting. GraphQL bukan hanya sebuah antarmuka, tetapi juga sebuah implementasi. Artinya, server-server GraphQL yang berbeda, dari berbagai penyedia, mungkin dirancang untuk memprioritaskan karakteristik yang berbeda pula. Dengan demikian, kita tidak selalu dapat memiliki ekspektasi yang seragam tentang apa yang ditawarkan GraphQL, atau pemahaman lengkap tentang cara kerja sebuah mesin GraphQL.

Misalnya, pengalaman GraphQL di WordPress dan di Laravel akan berbeda, begitu pula pengalaman yang diberikan oleh server-server yang berbeda, WPGraphQL atau Gato GraphQL.

Artikel ini adalah pendapat saya tentang masalah tersebut, yang membahas beberapa komentar dari posting Reddit.

GraphQL vs WP REST API

[Ide yang buruk sekali] memiliki GraphQL API di atas WordPress yang sudah menggunakan REST API-nya sendiri. Gunakan saja REST API. [Sumber]

Baik REST API maupun GraphQL melayani tujuan yang sama: menyediakan data yang dibutuhkan aplikasi. Namun, keduanya berperilaku berbeda dalam cara mencapai hal ini: sementara REST memiliki endpoint yang telah ditentukan sebelumnya yang menyediakan sekumpulan data tertentu, GraphQL dapat menyediakan tepat data yang dibutuhkan.

Perbedaan perilaku ini dapat berdampak langsung pada performa aplikasi. Dengan REST, jika kita perlu mengambil daftar posting beserta beberapa data dari setiap penulis posting tersebut, itu akan memerlukan pengiriman permintaan tambahan. Kemungkinan 1 permintaan tambahan untuk semua data penulis, atau 1 permintaan tambahan per penulis. Sementara itu, pengunjung situs web mungkin sedang menunggu halaman dirender.

GraphQL memperbaiki situasi ini, karena kita dapat langsung mengambil semua data posting dan penulis dalam satu permintaan, dan render halaman web akan lebih cepat:

{
  posts {
    id
    title
    excerpt
    date
    url
    author {
      id
      name
      url
    }
  }
}

Kemudian, meskipun kita sudah memiliki REST API di WordPress, itu tidak berarti REST API selalu menjadi alat yang paling tepat untuk setiap tugas. Tentu, kita selalu bisa menggunakannya, tetapi jika kita juga memiliki akses ke GraphQL, maka kita dapat memutuskan untuk menggunakan API ini setiap kali memberikan keunggulan dibanding REST, dan kita akan lebih diuntungkan.

Pengaturan Awal yang Sulit untuk GraphQL + Harus Menulis Resolver

Memang ada argumen yang bisa dibuat bahwa pengaturan awal untuk GraphQL jauh lebih tinggi dibandingkan REST; Anda benar bahwa asosiasi harus dikonfigurasi. [Sumber]

Dan...

Yang Anda dan hampir semua orang lain di web lewatkan adalah bahwa agar format API ini berfungsi, Anda harus menulis parser (resolver + tipe) yang membawa segudang masalah yang tidak ada pada REST. [Sumber]

Komentar-komentar ini tidak sepenuhnya akurat, karena baik WPGraphQL maupun Gato GraphQL telah memetakan model data WordPress ke dalam skema GraphQL (WPGraphQL sepenuhnya, plugin saya sebagian besar).

Kemudian, setelah menginstal salah satu plugin ini, Anda dapat langsung mulai mengambil data untuk aplikasi Anda, tanpa perlu membuat resolver apa pun, atau harus mengatur asosiasi antar entitas.

Memang benar bahwa, untuk mengambil data kustom dari entitas milik aplikasi sendiri (seperti dari CPT), entitas-entitas tersebut perlu dipetakan melalui resolver, dan Anda perlu melakukannya. Namun ini tidak berbeda dengan REST: jika Anda membutuhkan data kustom dari CPT Anda, Anda perlu membuat endpoint REST untuk mengambil data kustom tersebut. Endpoint kustom juga merupakan resolver.

Oleh karena itu, menyangkut kebutuhan resolver, REST dan GraphQL API pada dasarnya sama.

Sekarang, dari menelusuri situs web dan dokumentasi, memang memberikan kesan bahwa GraphQL membutuhkan lebih banyak upaya untuk dikonfigurasi. Jadi ada kebenarannya dalam asumsi ini.

Saya percaya ada beberapa alasan untuk ini. Pertama, GraphQL melibatkan (setidaknya) dua bagian:

  1. konsep tentang apa itu, dan cara kerjanya
  2. server-server yang menyediakan implementasi nyata

Saat menelusuri dokumentasi GraphQL, seperti situs resmi graphql.org, fokusnya ada pada konsep di balik GraphQL, membahas secara rinci tentang resolver, apa itu dan mengapa diperlukan.

Ini berguna ketika Anda membangun aplikasi dari awal, seperti menggunakan Laravel dan Lighthouse. Dalam kasus itu, Anda memang perlu membuat kode resolver Anda (tetapi Anda juga perlu membuat endpoint REST Anda).

Namun, WordPress sudah menjadi aplikasinya, dan WPGraphQL serta Gato GraphQL adalah solusinya. Kedua plugin ini telah membuat resolver untuk kita, sehingga kita tidak perlu khawatir tentang mereka (mirip dengan WP REST API yang juga menyediakan serangkaian endpoint awal, sehingga kita tidak perlu khawatir tentang mereka).

Selain itu, GraphQL lebih berpusat pada pengembang, dan dokumentasinya tampaknya berbicara langsung kepada para pengembang. Pengembang membuat resolver di sisi server, dan pengembang mengonsumsi resolver tersebut dengan query kustom di sisi klien. Karena membangun resolver adalah tugas pengembang, hal ini muncul secara alami dan sering.

Untuk REST, ekspektasinya (menurut saya) adalah bahwa endpoint yang menyediakan data yang diperlukan sudah ada (seperti yang disediakan oleh WP REST API). Jika tidak ada, barulah kita perlu mengkhawatirkan pengaturan endpoint kustom. Oleh karena itu, ada penekanan yang lebih sedikit pada pembuatan resolver untuk REST.

Jadi, pada akhirnya, REST dan GraphQL sama-sama menyediakan data yang diperlukan. Tetapi sementara REST mendorong pendekatan statis, di mana endpoint seharusnya sudah ada, dan hanya bila tidak ada kita mengkhawatirkannya, GraphQL mendorong pendekatan dinamis, di mana setiap query dibuat khusus, dan kemudian kita dapat membuat kode resolver yang sempurna untuknya.

Jadi, pada akhirnya, tidak ada perbedaan mendasar antara REST dan GraphQL, hanya interpretasi yang berbeda tentang bagaimana mereka harus memenuhi persyaratan mereka.

Kerentanan + Pertimbangan Keamanan dalam GraphQL

Kita akan melihat kerentanan besar dari GraphQL suatu hari nanti karena menulis interpreter yang aman sangat sulit. [Sumber]

Dan...

WordPress sudah begitu masif sehingga sudah memiliki target besar di punggungnya; memasang plugin APA PUN menambah banyak risiko, dan plugin yang menawarkan untuk mengekspos secara harfiah seluruh WordPress, termasuk banyak sampel kode untuk melewati model keamanan, adalah tidak untuk saya. Output yang tidak digerakkan oleh tema seharusnya dibatasi sebisa mungkin (tidak ada kecuali saya memintanya) di luar apa yang benar-benar perlu diekspos. Saya harap ini tidak pernah masuk ke core. [Sumber]

GraphQL memang menimbulkan risiko keamanan tambahan yang perlu kita tangani. Saya sepenuhnya setuju dengan perasaan ini.

Tetapi saya tidak berpikir ini adalah masalah yang begitu menghambat, hingga mencegah kemungkinan dimasukkannya GraphQL ke dalam core WP. Selain itu, saya bahkan tidak berpikir itu benar-benar sulit untuk diatasi.

Yang dibutuhkan adalah server GraphQL untuk memanfaatkan mekanisme keamanan yang ada di WordPress, dan kemudian pengembang menggunakan mekanisme ini, memastikan bahwa suatu field hanya dapat diakses oleh pengguna yang sesuai saja:

  • apakah pengguna sudah login?
  • apakah pengguna adalah admin?
  • apakah pengguna memiliki peran atau capability tertentu?
  • apakah pengguna adalah penulis posting tersebut?

Untuk memenuhi proposisi ini, Gato GraphQL menawarkan Access Control Lists, sehingga kita dapat mendefinisikan siapa yang dapat mengakses setiap field dan directive, melalui konfigurasi.

Sekarang, terkadang menggunakan ACL saja tidak cukup, dan server GraphQL perlu menyediakan langkah-langkah keamanan tambahan. Saya akan menggambarkan apa yang sedang saya kerjakan saat ini untuk v0.8 Gato GraphQL yang akan datang.

Field posts (untuk mengambil data posting) tidak memerlukan otorisasi, pengguna mana pun dapat mengaksesnya, baik yang sudah login maupun belum. Oleh karena itu, demi alasan keamanan, field ini hanya mengambil posting yang dipublikasikan.

Namun ada situasi di mana kita perlu mengambil posting draft/tertunda/dihapus juga, seperti:

  • Untuk membangun situs web statis, yang dijalankan oleh admin, dengan akses ke semua data dari situs
  • Untuk penulis posting, untuk membuat daftar semua posting draft agar mereka dapat terus mengeditnya

Kemudian, saya mengembangkan skema berikut. Untuk mengambil posting, akan ada 3 field:

  • posts: terbuka untuk siapa saja, hanya dapat mengambil posting yang dipublikasikan
  • myPosts: terbuka untuk siapa saja, hanya mengambil posting dari pengguna yang login, dengan status apa pun (dipublikasikan/draft/tertunda/dihapus)
  • postsForAdmin: hanya admin yang dapat mengaksesnya, mengambil posting apa pun dengan status apa pun

Dan kemudian, postsForAdmin secara default dinonaktifkan, sehingga bahkan tidak muncul di skema GraphQL, kecuali admin mengaktifkannya secara eksplisit (dan, kemungkinan besar, itu akan diaktifkan hanya untuk membangun situs statis).

Situasi lain adalah ketika suatu field dapat mengambil data publik maupun privat. Misalnya, field option mengambil data dari tabel wp_options. Beberapa entri bersifat publik (seperti blogname), sementara yang lain tidak (seperti admin_email).

Situasi serupa adalah untuk mengambil nilai meta, melalui field Post.metaValue, User.metaValue, dan lainnya. Misalnya, meta pengguna mencakup entri wp_capabilities, yang tentu saja bersifat privat, sementara description bersifat publik. Dan kemudian ada last_name, yang bisa bersifat publik atau privat tergantung pada aplikasi.

Untuk membuat akses ke data ini aman, plugin akan memungkinkan untuk menentukan entri mana yang dapat di-query melalui allow/denylist di halaman pengaturan, menerima baik entri lengkap maupun regex:

Mendefinisikan entri yang diizinkan/ditolak untuk field 'option'

Kemudian, melakukan query pada opsi yang diizinkan akan berhasil, sementara opsi yang ditolak hanya akan mengembalikan null:

{
  # This option is allowed
  siteName: optionValue(name: "blogname")
  # This optionValue is not allowed
  adminEmail: optionValue(name: "admin_email")
}

Dengan langkah-langkah keamanan yang tepat yang disediakan oleh server GraphQL, dan akal sehat dari pengembang, membuat GraphQL API yang aman tidak seharusnya sulit.

GraphQL Membebani Database

GraphQL adalah sintaks yang kaya yang memungkinkan query relasional mendalam untuk diekspresikan, sehingga untuk ekosistem seperti WordPress, di mana ekstensibilitas model data berasal dari pola entity-attribute-value, ini menghasilkan jumlah keausan yang luar biasa pada database, yang dapat menyebabkan situs Anda menjadi tidak responsif jika query GraphQL-nya dalam, rumit, atau rekursif. WordPress sudah terkenal karena dapat membuat instance MySQL/MariaDB bertekuk lutut, sehingga menambahkan GraphQL dapat memperburuk ini jika query-nya tidak ditulis, diautentikasi, dan dibatasi rate-nya dengan benar. [Sumber]

Membebani database adalah kekhawatiran serius untuk server GraphQL. Saya akan menggambarkan bagaimana Gato GraphQL berupaya menghindari skenario ini.

Gato GraphQL menghindari masalah N+1 agar tidak pernah terjadi, sudah berdasarkan desain arsitektur. Ini dicapai dengan membuat mesin bertanggung jawab untuk memuat entitas dari database, bukan pengembang.

Saat menyelesaikan koneksi dalam sebuah resolver, nilai yang dikembalikan adalah ID (atau daftar ID) dari objek tersebut, bukan objeknya sendiri. Misalnya, mengambil penulis dari custom post dilakukan seperti ini:

class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
  private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
 
  public function getClassesToAttachTo(): array
  {
    return [
      CustomPostFieldInterfaceResolver::class,
    ];
  }
 
  public function getSchemaFieldType(string $fieldName): ?string
  {
    return match($fieldName) {
      'author' => SchemaDefinition::TYPE_ID,
      default => null,
    };
  }
 
  public function resolveValue(
    TypeResolverInterface $typeResolver,
    object $customPost,
    string $fieldName,
    array $fieldArgs = []
  ): mixed {
    switch ($fieldName) {
      case 'author':
        return $this->customPostUserTypeAPI->getAuthorID($customPost);
    }
 
    return null;
  }
 
  public function resolveFieldTypeResolverClass(
    TypeResolverInterface $typeResolver,
    string $fieldName
  ): ?string {
    switch ($fieldName) {
      case 'author':
        return UserTypeResolver::class;
    }
 
    return null;
  }
}

Dengan memiliki ID entitas DB dari resolveValue, dan tipe objek dari resolveFieldTypeResolverClass (direpresentasikan melalui kelas UserTypeResolver), mesin GraphQL kemudian dapat memuat data untuk objek tersebut.

Untuk memuat data, mesin menggunakan algoritma yang sangat efisien: ini memiliki kompleksitas waktu O(n), di mana n adalah jumlah tipe dalam query, bukan jumlah node.

Algoritma mencapai efisiensi ini karena tidak melintasi sebuah graph, melainkan mengubah struktur data menjadi tumpukan komponen, yang jauh lebih sederhana untuk diselesaikan. ("Graph" dalam GraphQL adalah konsep, bukan implementasi nyata.)

Kemudian, meskipun query memiliki banyak level, masing-masing mengambil banyak entitas, algoritma masih dapat menanganinya dengan cukup baik. Misalnya, tidak ada dampak besar saat menjalankan query berikut, yang memiliki kedalaman 10 level:

{
  posts(pagination: { limit: 10 }) {
    excerpt
    title
    url
    author {
      name
      url
      posts(pagination: { limit: 10 }) {
        title
        tags(pagination: { limit: 10 }) {
          slug
          url
          posts(pagination: { limit: 10 }) {
            title
            comments(pagination: { limit: 10 }) {
              content
              date
              author {
                name
                posts(pagination: { limit: 10 }) {
                  title
                  url
                  comments(pagination: { limit: 10 }) {
                    content
                    date
                    author {
                      name
                      username
                      url
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Pengecualian untuk efisiensi ini adalah saat mengambil nilai meta, melalui Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue dan PostCategory.metaValue (dan juga field metaValues mereka). Itu karena fungsi-fungsi WordPress (get_post_meta, get_user_meta, dll) mengambil data untuk 1 ID sekaligus, yang berarti setiap entitas akan memerlukan panggilan database untuk mengambil nilai meta-nya. Akibatnya, penyelesaian nilai meta diskalakan berdasarkan jumlah node, bukan jumlah tipe (komentar OP tepat sasaran, dalam hal ini).

Untuk mencegah aktor jahat menggunakan dan menyalahgunakan field meta, Gato GraphQL (dalam v0.8) akan dikirimkan dengan field-field ini dinonaktifkan secara default. Kemudian, admin harus mengaktifkannya secara eksplisit dan, sambil melakukannya, dapat menempatkan field-field ini di bawah beberapa Access Control List, sehingga tidak pernah ada saat database berisiko diserang.

Rate limiting juga merupakan ide yang bagus, saya berencana untuk mendukungnya di beberapa rilis mendatang.

Dan kemudian ada menganalisis dan memberlakukan batasan pada kompleksitas query (seperti seberapa banyak level kedalamannya). Server GraphQL menyelesaikan query dengan kompleksitas waktu O(n), sehingga tidak banyak kerusakan yang dapat dilakukan menyangkut perulangan. Namun, satu query masih bisa mengambil jumlah data yang tidak terbatas dari database, dan itu adalah sesuatu yang mungkin ingin kita hindari.

Misalnya, query sederhana ini akan membawa sejumlah besar data dalam satu permintaan (situs demo saya hampir tidak memiliki beberapa ratus catatan, sehingga saya mampu mendemonstrasikan eksekusi query):

{
  posts000: posts(pagination: { limit: 100 }) {
    ...PostFields
  }
  posts100: posts(pagination: { limit: 100, offset: 100 }) {
    ...PostFields
  }
  posts200: posts(pagination: { limit: 100, offset: 200 }) {
    ...PostFields
  }
  posts300: posts(pagination: { limit: 100, offset: 300 }) {
    ...PostFields
  }
  posts400: posts(pagination: { limit: 100, offset: 400 }) {
    ...PostFields
  }
  posts500: posts(pagination: { limit: 100, offset: 500 }) {
    ...PostFields
  }
  posts600: posts(pagination: { limit: 100, offset: 600 }) {
    ...PostFields
  }
  posts700: posts(pagination: { limit: 100, offset: 700 }) {
    ...PostFields
  }
  posts800: posts(pagination: { limit: 100, offset: 800 }) {
    ...PostFields
  }
  posts900: posts(pagination: { limit: 100, offset: 900 }) {
    ...PostFields
  }
}
 
fragment PostFields on Post {
  id
  title
  content
  date
}

Seperti yang dapat dilihat, query bahkan tidak perlu bersarang untuk menimbulkan masalah. Jadi menganalisis kompleksitas sebuah query adalah urusan yang rumit, yang akan memerlukan penyesuaian halus agar berguna.

Saya berharap dapat mendukung analisis query juga, tetapi itu tidak ada dalam daftar prioritas tinggi saya, karena dengan kombinasi fitur-fitur lainnya (seperti persisted queries atau custom endpoints, dikombinasikan dengan Access Control Lists) kita sudah dapat menjauhkan aktor jahat, dan kita sendiri tidak akan (seharusnya tidak!) menyalahgunakan layanan GraphQL kita sendiri.


Berlangganan newsletter kami

Tetap update dengan semua pembaruan Gato GraphQL.