Konsep, Ide, Strategi
Konsep, Ide, StrategiMerancang aplikasi agar dapat bekerja dengan server GraphQL yang berbeda

Merancang aplikasi agar dapat bekerja dengan server GraphQL yang berbeda

"Coding terhadap antarmuka, bukan implementasi" adalah praktik memanggil suatu fungsionalitas tidak secara langsung, melainkan melalui sebuah kontrak yang menyebutkan input apa yang dibutuhkan dan apa output yang diharapkan, sambil menyembunyikan cara implementasinya dilakukan. Strategi ini membantu memisahkan aplikasi dari implementasi, penyedia, atau stack tertentu, sehingga memungkinkan perpindahan di antara mereka tanpa harus mengubah kode aplikasi.

Kita dapat menerapkan strategi ini dengan GraphQL juga. GraphQL dapat bertindak sebagai perantara antara aplikasi dan server, memungkinkan kita untuk melakukan semua modifikasi yang diperlukan hanya pada query GraphQL, sementara logika bisnis tetap tidak berubah.

Sebuah query GraphQL bertindak sebagai antarmuka antara klien dan server. Saat menjalankan sebuah query, server GraphQL akan memprosesnya dan mengembalikan data yang dibutuhkan ke klien. Dari mana data berasal? Bagaimana cara mendapatkannya? Klien tidak tahu, dan tidak peduli.

Query GraphQL bertindak sebagai antarmuka antara klien dan server

Respons terhadap query akan memiliki bentuk yang sama dengan query tersebut. Untuk query GraphQL ini:

{
  post(by: { id: 1 }) {
    id
    title
  }
}

...responsnya akan berupa:

{
  "data": {
    "post": {
      "id": 1,
      "title": "Hello world!"
    }
  }
}

Dengan query yang sama namun parameter berbeda, data yang dikembalikan akan berbeda, tetapi bentuknya akan tetap sama. Ini berarti bahwa, selama query tidak berubah, aplikasi tidak perlu mengubah logikanya dalam membaca dan memproses data, dan sama halnya tidak akan menjadi masalah server GraphQL mana yang menjalankan query tersebut.

Dengan demikian kita dapat dengan mudah berpindah dari satu server GraphQL ke server lainnya.

Query bergantung pada skema GraphQL

Kini, paragraf terakhir tadi sedikit terlalu optimis, karena query GraphQL mungkin perlu berubah tergantung pada server GraphQL-nya. Lebih tepatnya, query didasarkan pada skema GraphQL, dan jika server yang berbeda mengekspos skema yang berbeda maka query pun akan berbeda.

Misalnya, sebuah server GraphQL yang menggunakan Cursor Connections Specification mungkin menjalankan query berikut:

{
  categories(first: 10000) {
    edges {
      node {
        categoryId
        description
        id
        name
        slug
      }
    }
  }
}

Dan server lain yang menggunakan pagination gaya WordPress (seperti Gato GraphQL) akan menjalankan query yang sama seperti ini:

{
  postCategories(pagination: { limit: 10000 }) {
    id
    description
    globalID
    name
    slug
  }
}

Kita dapat melihat perbedaan antara kedua query tersebut:

FiturServer #1Server #2
Field post categoriescategoriespostCategories
Argumen field untuk membatasi jumlah hasilfirstpagination.limit
Field id sebuah objek merepresentasikanID global uniknyaID uniknya untuk tipenya
Bentuk querylebih dalam karena edges.nodelebih datar

Mengganti query dari server pertama dengan yang setara dari server kedua di dalam aplikasi saja tidak akan berhasil. Itu karena logikanya masih akan mengakses data dari respons sesuai dengan bentuk dan field dari query asli.

Satu solusi yang mungkin adalah juga mengganti logika untuk mengambil data di klien. Misalnya, logika berikut:

const categories = data?.data.categories.edges.map(({ node = {} }) => node);

...dapat diganti seperti ini:

const categories = data?.data.postCategories;

Namun itulah yang justru ingin kita hindari. Kita ingin menjaga perubahan seminimal mungkin, hanya memodifikasi antarmuka (query GraphQL), dan membiarkan logika bisnis tidak berubah.

Untungnya, dimungkinkan untuk menjembatani perbedaan tersebut hanya dengan memodifikasi query GraphQL, mengikuti langkah-langkah berikut:

  1. Memisahkan query GraphQL dari aplikasi
  2. Mengadaptasi nama field melalui alias
  3. Mengadaptasi bentuk respons melalui field self

Mari kita lihat bagaimana, melalui 3 langkah ini, kita dapat mengadaptasi sebuah aplikasi untuk mengarah ke server GraphQL yang berbeda.

Memisahkan query GraphQL dari aplikasi

Memisahkan query GraphQL dari logika aplikasi melibatkan:

  • Menyimpan setiap query GraphQL (atau sekumpulan di antaranya) dalam file terpisah, dan semuanya dalam folder tertentu
  • Mengekspor query tersebut dan mengimpornya ke dalam aplikasi

Misalnya, kita dapat menempatkan setiap query GraphQL dalam file terpisah di bawah src/data, dan mengekspornya:

// file `src/data/categories.js`
export const QUERY_ALL_CATEGORIES = gql`
  {
    categories(first: 10000) {
      edges {
        node {
          databaseId
          description
          id
          name
          slug
        }
      }
    }
  }
`;

Aplikasi kemudian dapat mengimpor dan menggunakan query GraphQL:

import { QUERY_ALL_CATEGORIES } from 'data/categories';
 
export async function getAllCategories() {
  const apolloClient = getApolloClient();
 
  const data = await apolloClient.query({
    query: QUERY_ALL_CATEGORIES,
  });
 
  const categories = data?.data.categories.edges.map(({ node = {} }) => node);
 
  return {
    categories,
  };
}

Berkat pengaturan ini, semua modifikasi hanya perlu dilakukan pada file-file di bawah src/data.

Mengadaptasi nama field melalui alias

Alias field dapat digunakan untuk mengganti nama sebuah field dalam respons dari server GraphQL kedua menjadi nama field tersebut di server pertama.

Dengan cara ini, field postCategories, id, dan globalID dapat diambil menggunakan nama yang diharapkan oleh aplikasi: masing-masing categories, categoryId, dan id:

{
  categories: postCategories(pagination: { limit: 10000 }) {
    categoryId: id
    description
    id: globalID
    name
    slug
  }
}

Perlu diperhatikan bahwa field categories memiliki argumen first, sementara field yang bersesuaian postCategories menggunakan argumen pagination.limit. Namun, karena argumen field tidak tercermin dalam nama field pada respons, kita tidak perlu mengkhawatirkan hal tersebut.

Mengadaptasi bentuk respons melalui field self

Tantangan terakhir sedikit lebih rumit: kita perlu memodifikasi bentuk respons, menambahkan level ekstra untuk edges dan node yang berasal dari spec Cursor Connections.

Untuk mencapai ini, kita akan memperkenalkan field self ke semua tipe dalam skema GraphQL, yang mengembalikan kembali objek yang sama tempat field ini diterapkan:

type QueryRoot {
  self: QueryRoot!
}
 
type Post {
  self: Post!
}
 
type User {
  self: User!
}

Field self memungkinkan penambahan level ekstra ke query tanpa meninggalkan objek yang di-query. Menjalankan query ini:

{
  __typename
  self {
    __typename
  }
  
  post(by: { id: 1 }) {
    self {
      id
      __typename
    }
  }
  
  user(by: { id: 1 }) {
    self {
      id
      __typename
    }
  }
}

...menghasilkan respons ini:

{
  "data": {
    "__typename": "QueryRoot",
    "self": {
      "__typename": "QueryRoot"
    },
    "post": {
      "self": {
        "id": 1,
        "__typename": "Post"
      }
    },
    "user": {
      "self": {
        "id": 1,
        "__typename": "User"
      }
    }
  }
}

Sekarang, kita dapat menggunakan self untuk secara artifisial menambahkan level nodes dan edge:

{
  categories: self {
    edges: postCategories(pagination: { limit: 10000 }) {
      node: self {
        categoryId: id
        description
        id: globalID
        name
        slug
      }
    }
  }
}

Tipe objek dalam skema GraphQL untuk edges dan untuk self jelas berbeda. Namun hal itu tidak menjadi masalah bagi aplikasi, karena aplikasi tidak berinteraksi dengan objek aktual yang dimodelkan di server GraphQL. Sebaliknya, ia menerima data sebagai objek JSON, dan potongan data untuk sebuah field yang berasal dari objek PostConnection maupun objek Post akan sama.

Perlu diperhatikan bahwa field categories diselesaikan melalui self dan edges diselesaikan melalui postCategories, bukan sebaliknya. Ini untuk menjaga kardinalitas elemen yang dikembalikan sesuai dengan yang didefinisikan oleh field yang menggunakan spec Cursor Connections:

type RootQuery {
  categories: RootQueryToCategoryConnection
}
 
type RootQueryToCategoryConnection {
  edges: [RootQueryToCategoryConnectionEdge]
}
 
type RootQueryToCategoryConnectionEdge {
  node: Category
}

Jika query GraphQL yang diadaptasi dilakukan sebaliknya (yaitu melakukan query categories: postCategories dan edges: self), pengaksesan data akan gagal, karena data.categories akan berupa array, sehingga data.categories.edges akan melempar error saat menjalankan:

const categories = data?.data.categories.edges.map(({ node = {} }) => node);

Mengadaptasi semua query

Setelah menerapkan strategi yang sama ke semua query GraphQL di src/data, aplikasi dapat dengan mudah berpindah dari satu server GraphQL ke server lainnya.