Resolver GraphQL adalah: Fungsi dan Contoh — GraphQL

Resolver adalah fungsi yang menyediakan data untuk tiap field di schema GraphQL. Pelajari signature parent, args, context, info lewat contoh kode.

Resolver adalah fungsi di server yang mengisi nilai satu field. Schema cuma bilang field apa saja yang ada; resolver yang mencari isinya, misalnya dari database. Bayangin pesanan di toko buku dikerjakan berantai: petugas pertama mengambil bukunya dari rak, lalu menyerahkannya ke petugas berikutnya yang mencari data penulisnya, lalu ke petugas yang mencari buku-buku lain penulis itu. Tiap petugas mulai dari barang yang diserahkan petugas sebelumnya. Itu rantai resolver.

🧪 Tebakan cepat: Resolver Book.author dihapus dari server Toko Buku. Query { book(id: 1) { title author { name } } } menjawab apa? Jawabannya ada di bagian "Resolver default: membaca property".

Empat parameter: parent, args, context, info

Ini sebagian resolver Toko Buku, persis yang dijalankan Lab di halaman ini:

const resolvers = {
  Query: {
    book: (_, args) => books.find((b) => String(b.id) === args.id) ?? fail("Book not found"),
  },
  Book: {
    status: (book) => statusOf(book),
    author: (book) => authors.find((a) => a.id === book.authorId),
  },
  Author: {
    books: (author) => books.filter((b) => b.authorId === author.id),
  },
};
Parameter Isinya Contoh
parent hasil resolver satu tingkat di atasnya di Book.author: baris buku { id: 1, title: "Laskar Pelangi", authorId: 1, price: 89000, stock: 12 }
args argument field ini, sudah dicek type-nya di Query.book: { id: "1" }, string karena type-nya ID
context objek bersama untuk satu request { user, db }
info keterangan field yang sedang diisi info.fieldName = "author", info.path = book, lalu author

Di field teratas (Query.book), parent adalah root value, biasanya undefined. Makanya ditulis _.

Rantai resolver: book → author → books

{
  book(id: 1) {
    title
    author {
      name
      books {
        title
      }
    }
  }
}

Mesin GraphQL berjalan dari atas ke bawah. Hasil tiap resolver jadi parent untuk field di dalamnya:

# Field parent yang diterima Resolver Hasilnya
1 Query.book undefined, args { id: "1" } ditulis sendiri baris buku 1
2 Book.title baris buku 1 default "Laskar Pelangi"
3 Book.author baris buku 1 ditulis sendiri { id: 1, name: "Andrea Hirata" }
4 Author.name penulis 1 default "Andrea Hirata"
5 Author.books penulis 1 ditulis sendiri list berisi buku 1
6 Book.title baris buku 1 default "Laskar Pelangi"

Enam pemanggilan: tiga resolver yang ditulis sendiri, tiga resolver default. Response-nya { "book": { "title": "Laskar Pelangi", "author": { "name": "Andrea Hirata", "books": [{ "title": "Laskar Pelangi" }] } } }.

Resolver default: membaca property

Field tanpa resolver tetap terisi, karena graphql-js memakai resolver default: ia membaca property bernama sama dari parent (parent.title), dan memanggilnya kalau property itu fungsi. Itu sebabnya title, price, stock, dan name gak butuh resolver.

Book.author berbeda. Baris buku menyimpan authorId, bukan author. Kalau resolver-nya dihapus, resolver default membaca book.author, hasilnya undefined, dan author bertype Author! gak boleh kosong:

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Book.author.",
      "locations": [{ "line": 4, "column": 5 }],
      "path": ["book", "author"]
    }
  ],
  "data": { "book": null }
}

null dari author naik ke tempat terdekat yang boleh null, yaitu book. Jadi judulnya pun hilang. Itu jawaban tebakannya. Kalau resolver melempar error di field non-null, null-nya naik dengan cara yang sama, cuma pesannya pesan error itu. Aturannya: tulis resolver untuk field yang nilainya gak ada apa adanya di parent (relasi seperti author, nilai hitungan seperti status), selebihnya biarkan default.

Resolver async

Resolver sungguhan biasanya membaca database, jadi ia async dan mengembalikan Promise. graphql-js menunggu Promise itu sebelum mengisi field di bawahnya:

const resolvers = {
  Query: {
    books: async (_, args, context) => context.db.books.findMany(),
  },
  Book: {
    author: async (book, _, context) => context.db.authors.findById(book.authorId),
  },
};

Ada satu efek yang perlu kamu tahu. Untuk { books { title author { name } } }, mesin gak menunggu satu buku selesai sebelum mulai buku berikutnya. Dengan resolver async, urutan pemanggilannya jadi begini: Query.books, lalu Book.title dan Book.author untuk buku 1, 2, dan 3, baru Author.name tiga kali setelah ketiga penulis datang. Book.author dipanggil tiga kali, sekali per buku. Kalau tiap pemanggilan itu satu query database, itu masalah N+1, dibahas di pelajaran "N+1 Problem & DataLoader". Field teratas mutation tetap dijalankan berurutan walaupun resolver-nya async.

context: dibuat baru untuk tiap request

context dibuat server sekali per request, lalu diberikan ke semua resolver request itu. Di Apollo Server:

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({
    user: await userFromToken(req.headers.authorization),
    db,
  }),
});

Logika bisnis di luar resolver

Resolver sebaiknya tipis: baca args dan context, panggil fungsi bisnis, kembalikan hasilnya. Aturan seperti "stok gak boleh negatif" ditaruh di fungsi bisnis, misalnya updateStock: (_, { id, stock }, { services }) => services.stock.update(id, stock). Dengan begitu aturan yang sama dipakai juga oleh REST API, job terjadwal, dan test, tanpa ditulis ulang.

⚠️ Jebakan yang sering ketemu

TL;DR: Resolver adalah fungsi yang mengambil data untuk tiap field di schema, dikelompokkan per type dan field (Book.author). Parameternya selalu (parent, args, context, info): hasil resolver di atasnya, argument field itu, objek bersama satu request, dan keterangan field. Resolver berantai: Query.book mengembalikan baris buku, yang jadi parent untuk Book.author, yang hasilnya jadi parent untuk Author.books. Field tanpa resolver memakai resolver default yang membaca parent[namaField]; field relasi seperti author butuh resolver sendiri, dan tanpa itu null di field non-null naik ke tempat terdekat yang boleh null. Resolver boleh async; saudara-saudaranya dimulai tanpa saling menunggu. context dibuat baru untuk tiap request, dan logika bisnis sebaiknya di luar resolver.

Yang akan kamu pelajari