Konsep, Ide, Strategi
Konsep, Ide, StrategiBagaimana plugin memetakan model data WordPress ke skema GraphQL

Bagaimana plugin memetakan model data WordPress ke skema GraphQL

Inilah cara Gato GraphQL memetakan model data WordPress ke skema GraphQL yang bersesuaian.

Model data WordPress

WordPress memiliki entitas-entitas berikut:

  • posts
  • pages
  • custom posts
  • elemen media
  • pengguna
  • peran pengguna
  • tag
  • kategori
  • komentar
  • blok
  • properti meta
  • lainnya (opsi, plugin, tema, dll.)

Entitas-entitas ini dapat memiliki hierarki. Misalnya, post, page, dan elemen media semuanya merupakan custom post types, dan tag serta kategori keduanya merupakan taksonomi.

Ini adalah diagram basis data WordPress, yang menunjukkan bagaimana data untuk semua entitas disimpan:

Diagram basis data WordPress

Apakah pemetaan merupakan replika persis dari diagram BD?

Saat memetakan basis data WordPress ke dalam skema GraphQL, apakah diagram yang sama di atas dihormati secara 1 banding 1?

Tidak, tidak demikian. Meskipun diagram basis data adalah implementasi nyata, GraphQL adalah antarmuka untuk mengakses data dari klien. Keduanya saling berkaitan, tetapi bisa berbeda. GraphQL tidak peduli dengan basis data: ia tidak berpikir dalam perintah SQL, atau mengetahui adanya tabel basis data bernama wp_posts dan wp_users.

Jadi kita tidak perlu terlalu khawatir tentang diagram basis data saat membuat skema GraphQL untuk WordPress. Bahkan, kita dapat menghasilkan skema GraphQL yang memperbaiki sebagian utang teknis dari model data WordPress.

Memetakan model data WordPress sebagai skema GraphQL

Mari kita lakukan pemetaan. Pertama, kita petakan entitas asli sebagai tipe, sebisa mungkin. Dari daftar entitas dalam model data WordPress, kita menghasilkan tipe-tipe berikut untuk skema GraphQL:

  • Post
  • Page
  • Media
  • User
  • UserRole
  • PostTag
  • PostCategory
  • Comment

Kemudian, kita tambahkan semua field yang diharapkan ke setiap tipe. Untuk merepresentasikan skema, kita dapat menggunakan SDL, atau Schema Definition Language. (Ini hanya digunakan untuk keperluan dokumentasi; plugin itu sendiri tidak menggunakan SDL untuk mengodekan skema: semuanya adalah kode PHP).

Ini adalah field-field (di antara banyak lainnya) untuk sebuah Post:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
  date: Date!
}

Ini adalah field-field (di antara banyak lainnya) untuk sebuah User:

type User {
  id: ID!
  name: String
  email: String!
}

Kita juga membuat koneksi yang bersesuaian, yaitu field yang mengembalikan entitas lain (alih-alih skalar, seperti angka atau string). Misalnya, kita merepresentasikan sebuah post yang memiliki penulis, dan seorang pengguna yang memiliki posts:

type Post {
  author: User!
}
 
type User {
  posts: [Post]
}

Field dan koneksi juga dapat menerima argumen. Misalnya, kita mengaktifkan Post.dateStr agar dapat diformat, dan User.posts agar dapat memfilter entri, membatasi jumlahnya, dan mengurutkannya:

type Post {
  dateStr(format: String): Date!
}
 
type User {
  posts(
    filter: RootPostsFilterInput
    pagination: PostPaginationInput
    sort: CustomPostSortInput
  ): [Post!]!
}
 
input RootPostsFilterInput {
  authorIDs: [ID!]
  authorSlug: String
  categoryIDs: [ID!]
  dateQuery: [DateQueryInput!]
  excludeAuthorIDs: [ID!]
  excludeIDs: [ID!]
  hasPassword: Boolean = false
  ids: [ID!]
  isSticky: Boolean
  metaQuery: [CustomPostMetaQueryInput!]
  password: String
  search: String
  status: [FilterCustomPostStatusEnum!]
  tagIDs: [ID!]
  tagSlugs: [String!]
}
 
input PostPaginationInput {
  limit: Int
  offset: Int
}
 
input CustomPostSortInput {
  by: CustomPostOrderByEnum
  order: OrderEnum
}
 
# ...

Kita terus melakukan ini untuk semua entitas dalam model data WordPress. Setelah selesai, kita akan tiba pada skema GraphQL untuk WordPress, yang terlihat menggunakan klien Voyager (tersedia sebagai "Interactive Schema" di menu plugin):

Skema GraphQL untuk WordPress

Skema ini memiliki kemiripan dengan diagram basis data WordPress, tetapi juga beberapa perbedaan. Mari kita analisis.

Operasi tanpa entitas dipetakan sebagai field Root

Diagram basis data WordPress merepresentasikan bagaimana data disimpan, sehingga tidak ada "awal". GraphQL, bagaimanapun, adalah antarmuka untuk mengambil data, sehingga harus ada tahap awal dari mana query dieksekusi.

Tahap awal ini adalah tipe Root, atau, lebih tepatnya, tipe QueryRoot dan MutationRoot (untuk menangani query dan mutasi, masing-masing).

Dalam dua tipe ini, kita petakan semua operasi yang tidak bergantung pada entitas, seperti saat mengeksekusi get_posts(), get_users() atau wp_signon():

type QueryRoot {
  posts: [Post]!
  users: [User]!
}
 
type MutationRoot {
  loginUser(
    usernameOrEmail: String!,
    password: String!
  ): User
}

Field-field tidak perlu memiliki nama atau tanda tangan yang sama dengan operasi yang mereka representasikan. Misalnya, memanggil field loginUser dapat dianggap lebih sesuai daripada signOn.

Mengelompokkan elemen skema

Kita dapat menerapkan peningkatan untuk menyederhanakan skema dan membuatnya lebih berguna. Misalnya, sebuah field dapat menerima semua argumennya melalui objek input, yang dapat digunakan kembali di beberapa field dan memudahkan visualisasi skema:

type MutationRoot {
  loginUser(input: LoginUserByInput!): User
}
 
input LoginUserByInput {
    usernameOrEmail: String!,
    password: String!
}

Selain itu, respons dari sebuah mutasi dapat berupa objek "payload", yang selain mengembalikan objek yang terpengaruh juga dapat menyertakan status operasi dan pesan kesalahan:

type MutationRoot {
  loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
 
type RootLoginUserMutationPayload {
  errors: [RootLoginUserMutationErrorPayloadUnion!]
  status: OperationStatusEnum!
  user: User
  userID: ID
}
 
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
  | InvalidUserEmailErrorPayload
  | InvalidUsernameErrorPayload
  | PasswordIsIncorrectErrorPayload
  | UserIsLoggedInErrorPayload

Semua mutasi berada di bawah MutationRoot

Ada operasi yang memang bergantung pada entitas, seperti wp_update_post(), yang diterapkan pada suatu post. Mutasi yang bersesuaian dalam skema GraphQL harus ditambahkan ke tipe MutationRoot, karena itulah cara kerja GraphQL.

Kemudian, operasi ini dipetakan seperti ini:

type MutationRoot {
  updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
 
input RootUpdatePostFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  id: ID!
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Plugin ini juga mendukung nested mutations, yang ditawarkan sebagai fitur opt-in (karena ini bukan perilaku standar GraphQL). Kemudian, mutasi juga dapat ditambahkan di bawah tipe mana pun, bukan hanya MutationRoot. Dalam kasus ini, kita mendapatkan:

type Post {
  update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
 
input PostUpdateFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Perhatikan perbedaan antara input RootUpdatePostFilterInput dan PostUpdateFilterInput (yaitu, antara mutasi dari root, dan nested mutations): yang pertama memiliki properti wajib id untuk menunjukkan post mana yang akan dimodifikasi, tetapi yang kedua tidak, karena tidak memerlukannya.

Menangani custom posts

Tidak ada pewarisan tipe dalam GraphQL. Oleh karena itu, kita tidak dapat memiliki tipe CustomPost, dan mendeklarasikan bahwa Post dan Page memperluasnya.

GraphQL menawarkan dua sumber daya untuk mengompensasi kekurangan ini: interface dan union types.

Untuk yang pertama, kita membuat interface CustomPost untuk skema, mendeklarasikan semua field yang diharapkan dari sebuah custom post, dan kita mendefinisikan tipe Post, Page, dan GenericCustomPost (untuk merepresentasikan semua custom post types yang didefinisikan oleh tema dan plugin yang terpasang) untuk mengimplementasikan interface tersebut:

interface CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Post implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Page implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type GenericCustomPost implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}

Untuk yang kedua, kita membuat tipe CustomPostUnion untuk skema yang mengembalikan semua custom post types:

union CustomPostUnion = Post | Page | GenericCustomPost

Dan membuat field-field mengembalikan tipe ini bila sesuai:

type QueryRoot {
  customPost(id: ID): CustomPostUnion
  customPosts: [CustomPostUnion]!
}
 
type User {
  customPosts: [CustomPostUnion]
}
 
type Comment {
  customPost: CustomPostUnion!
}

Saat mengeksekusi query, kita dapat memilih field berdasarkan tipe aktual, seperti Post, atau berdasarkan interface CustomPost:

{  
  customPosts {
    __typename
    ...on CustomPost {
      id
      title
      slug
      status
    }
    ...on Post {
      isSticky
      postFormat
    }
  }
}

Sebagaimana dapat diamati, dalam skema GraphQL kita perlu secara eksplisit menyatakan kapan kita berurusan dengan posts, dan kapan dengan custom posts, karena keduanya tidak sama! Menyebut keduanya secara bergantian adalah utang teknis dari WordPress, yang coba diperbaiki oleh plugin ini kapan pun memungkinkan.

Untuk alasan ini, sebuah custom post selalu disebut CustomPost dan bukan Post, sebuah field yang berurusan dengan custom posts selalu disebut customPosts dan bukan posts, dan sebuah argumen field yang menerima ID untuk custom post disebut customPostID dan bukan postID (meskipun itulah yang disebut dalam fungsi WordPress yang dipetakan).

Kemudian, ekspektasinya selalu jelas:

  • Field User.customPosts dapat mengembalikan daftar custom post apa pun, termasuk posts dan pages, dan User.posts hanya mengembalikan posts
  • Field Root.setFeaturedImageOnCustomPost dapat menambahkan gambar unggulan ke custom post mana pun, itulah mengapa tidak disebut setFeaturedImageOnPost

Tidak mengelompokkan tag (dan kategori) di bawah satu tipe

Mengapa tipe PostTag (dan sama halnya untuk PostCategory) disebut demikian, alih-alih sekadar Tag?

Karena, saat mengeksekusi query ini (di mana produk adalah CPT), hasil dari field tags untuk posts dan produk akan selalu berbeda, tidak tumpang tindih:

query {
  posts {
    tags {
      id
      name
    }
  }
  products {
    tags {
      id
      name
    }
  }
}

Tag yang ditambahkan ke posts tidak akan muncul saat mengambil tag untuk produk, dan sebaliknya (kecuali produk juga menggunakan taksonomi post_tag, tetapi dalam kasus itu juga dapat direpresentasikan dengan tipe PostTag). Ini tidak menjadi masalah besar di WordPress, karena item-item ini dapat dianggap sebagai baris berbeda dari tabel basis data yang sama. Tetapi hal itu penting untuk GraphQL, yang bertipe kuat.

Kemudian, keputusan desain yang baik adalah menjaga entitas-entitas ini terpisah, di bawah tipe mereka sendiri, dan memiliki tag untuk posts yang dikembalikan di bawah tipe PostTag dan, jika plugin kustom mengimplementasikan CPT produknya sendiri, plugin tersebut harus menggunakan tipe ProductTag untuk tag-tagnya.

Memberikan elemen media identitas mereka sendiri

Entitas media di WordPress adalah custom post types, hanya karena itu nyaman dari sudut pandang implementasi. Namun, skema GraphQL dapat menghindari utang teknis ini, dan memodelkan elemen media sebagai entitas yang berbeda, bukan sebagai custom posts.

Ini menyiratkan keputusan-keputusan berikut untuk skema GraphQL:

  • Tipe Media tidak mengimplementasikan interface CustomPost, dan tidak akan menjadi bagian dari tipe CustomPostUnion
  • Tipe Media tidak memiliki banyak field yang diharapkan dari sebuah custom post type, seperti excerpt, date, dan status. Sebaliknya, ia hanya memiliki field-field yang diharapkan dari sebuah elemen media:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Mengidentifikasi dan memetakan enum

Dalam beberapa situasi, WordPress menggunakan nilai-nilai tetap dari kumpulan tertentu. Misalnya, status sebuah post hanya dapat berupa "publish", "draft", "pending", atau "trash".

Dalam GraphQL, kita dapat memperlakukan ini sebagai enum (alih-alih string), dan membuat tipe enumerasi yang bersesuaian. Mengikuti standar GraphQL, enum harus ditulis dalam huruf besar, seperti ini:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Namun, query tersebut kemudian tidak dapat langsung digunakan untuk berinteraksi dengan WordPress, karena mengeksekusi get_posts( [ "post_status" => "PUBLISH" ] ) tidak berfungsi.

Jadi, sebagai kompromi, kita menjaga nilai-nilai enum ini dalam huruf kecil:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Memetakan tipe-tipe tambahan

Blok tidak terlihat langsung dalam diagram basis data WordPress, karena disimpan di wp_posts (tidak ada tabel wp_blocks), tetapi meskipun demikian mereka adalah entitas yang berbeda.

Oleh karena itu, kita tetap dapat memperkenalkan tipe Block untuk memetakannya:

type Post {
  blocks: [Block]
}
 
type Block {
  type: String!
  attributes: JSONObject
}