Mengintegrasikan Apple CloudKit dengan Next.js

5 Juli 2024  •  --------

Tersedia dalam .

Tulisan ini bermula dari pertanyaan yang muncul di benak saya. Bagaimana aplikasi bawaan Apple seperti Notes, Reminders, Photos, dapat berjalan di web, meskipun datanya bersifat privat dan tersimpan di dalam akun iCloud pengguna?

Ternyata jawabannya adalah CloudKit. CloudKit adalah sebuah framework database yang dibuat oleh Apple. Namun tidak hanya fokus dibuat untuk menyimpan data, CloudKit juga menganut prinsip "local-first". Ini berarti data akan disimpan secara lokal di perangkat Apple terlebih dahulu, yang kemudian dapat disinkronkan ke CloudKit database. CloudKit menyediakan 2 jenis database:

  • CloudKit Private Database: Penyimpanan data yang aman dan privat dalam akun iCloud pengguna. Cocok untuk aplikasi yang menyimpan data-data personal seperti Apple Notes, Reminders, dan Photos.
  • CloudKit Public Database: Penyimpanan data publik yang tidak terkait dengan akun iCloud tertentu. Sehingga semua orang dapat mengaksesnya. Cocok untuk aplikasi yang datanya dikonsumsi secara publik seperti Apple App Store, musik yang dapat didengarkan di Apple Music, juga film maupun series di Apple TV (ini hanya sebagai contoh penggunaan, bukan data sebenarnya).

Dalam tulisan ini, saya akan fokus membahas CloudKit Private Database. Saya akan berbagi pengalaman saya dan sedikit gambaran tentang cara mengintegrasikan CloudKit dengan aplikasi web. Meskipun saya menggunakan Next.js, kode JavaScript yang saya tulis disini bersifat umum dan dapat digunakan di framework lainnya.

Daftar Isi

Prasyarat

  1. Anda memiliki Apple Developer Account yang aktif. Biaya keanggotaannya adalah $99 USD per tahun.
  2. Anda sudah atau sedang mengembangkan aplikasi untuk platform Apple (iOS, macOS, atau yang lainnya) yang telah dikonfigurasi untuk menggunakan CloudKit.
  3. Anda familiar dengan bahasa pemrograman JavaScript dan Next.js.

Mulai

Apple menyediakan beberapa cara untuk mengakses data CloudKit dengan JavaScript. Beberapa caranya:

  • CloudKit Web Services - Ini adalah cara yang akan kita gunakan. Sebuah API service yang disediakan Apple untuk mengakses CloudKit. Menurut saya, cara ini merupakan pilihan terbaik saat ini.
  • CloudKit.js - Sebuah library JavaScript yang disediakan Apple. Namun, library ini sudah tidak diperbarui oleh Apple, sehingga beberapa orang menyarankan untuk tidak menggunakannya lagi (lihat salah satu contoh).
  • CKTool JS - Sebuah library Node.js yang dirilis Apple pada tahun 2022. Namun, library ini lebih berfokus untuk melakukan otomasi (lihat contoh).

Step 1 — Akses CloudKit Console

Langkah pertama yang harus kita lakukan adalah memastikan kita dapat melihat CloudKit container yang telah dikonfigurasi sebelumnya. Caranya:

  1. Buka CloudKit Console — icloud.developer.apple.com/dashboard.
  2. Login menggunakan akun Apple Developer Account Anda.
  3. Klik "CloudKit Database".
  4. Jika Anda melihat tampilan seperti ini, berarti Anda sudah berhasil mengakses CloudKit Console.
  5. Gunakan dropdown di pojok kiri atas untuk memilih project yang ingin Anda akses. Dropdown ini akan menampilkan bundle ID aplikasi Anda yang telah terintegrasi dengan CloudKit.

Mengaktifkan Query

Selanjutnya, kita perlu menambahkan satu index baru ke dalam pengaturan tabel agar kita dapat melakukan query ke tabel tersebut.

  1. Pilih menu "Schema""Indexes".

  2. Pastikan Anda melihat daftar Indexes yang sudah ada sebelumnya. Biasanya item-item ini memiliki format CD_<column_name>.

  3. Jika Anda sudah melihatnya, klik tombol tambah di atas.

  4. Lalu isi form seperti gambar dibawah ini.

    • Record Type - Pilih tabel yang Anda gunakan.
    • Name - Isi dengan nama yang mudah diingat. Biasanya, saya menggunakan nama yang sama dengan Field yang ada di bawah.
    • Type - Pilih QUERYABLE agar tabel kita dapat di query dari CloudKit Console.
    • Field - Pilih recordName dari dropdown.

Mencoba Melakukan Query

Setelah mengaktifkan query, kita perlu memastikan bahwa kita dapat melakukan query dari CloudKit Console.

  1. Pilih menu "Data""Records".

  2. Atur opsi untuk melakukan query:

    • Database: Pilih "Private Database".
    • Record Zone: Pilih "com.apple.coredata.cloudkit.zone". Ini adalah record zone yang dibuat oleh CoreData atau SwiftData pada saat kita mengintegrasikannya ke dalam iOS app.
    • Record Type (opsi berwarna biru di bawah opsi Database): Pilih tabel yang ingin Anda query. Dalam project saya, tabelnya bernama CD_Todo.

    Setelah semua sudah dipilih, klik "Query Records".

    Hasil query akan bervariasi tergantung seberapa banyak data yang Anda miliki. Jika Anda belum menambahkan data apapun, tampilannya akan kosong. Jika sudah, kurang lebih akan seperti gambar di atas.

Setelah Anda berhasil melakukan query, Anda juga dapat mengubah data langsung dari dashboard ini jika diperlukan.

Step 2 — CloudKit API Key

Untuk mengakses CloudKit dari aplikasi web, kita perlu membuat API key.

  1. Pilih menu "Settings""Token & Keys", lalu klik pada icon plus di sebelah tulisan "API Tokens".

  2. Isi form pembuatan API key:

    • Name - Beri nama yang mudah diingat, misalnya "Next.js API Key".

    • Sign in Callback

      • Pilih "URL Redirect", lalu pada dropdown pilih "localhost".
      • Isi port localhost Anda (default Next.js berjalan di port 3000).
      • Isi URL path, dalam contoh ini saya menggunakan /api/auth/callback.

    Biarkan pilihan lainnya.

  3. Jika sudah, simpan API key yang sudah dibuat di tempat yang aman.

Sangat penting untuk menjaga kerahasiaan API key ini. Jangan pernah membagikan atau mempublikasikan API key Anda.

Step 3 — Integrating CloudKit Web Services

Sekarang kita akan mulai mengintegrasikan CloudKit ke dalam aplikasi Next.js kita menggunakan CloudKit Web Services API. Kita akan mulai dari autentikasi, membuat API endpoints, dan yang terakhir memanggilnya dari sisi client (frontend).

Environment Variables

Beberapa data penting diperlukan untuk dapat mengintegrasikan CloudKit. Simpan semua data ini di environment variables.

  1. Apple CloudKit API Key - Data ini merupakan data sensitif dan hanya boleh digunakan di server. Jangan pernah menyertakannya dalam client bundle.
  2. Container - Nama bundle ID aplikasi Anda.
  3. Environment - Pilih antara development atau production. Seperti namanya ini cukup menjelaskan ya, jika Anda menggunakannya di dalam proses development, maka gunakan development. Lalu saat Anda sudah siap merilisnya, gunakan production.

Seperti ini contoh file .env nya:

.env
APPLE_CK_API_KEY=<api_key>
APPLE_CK_CONTAINER=iCloud.com.example.Todo
APPLE_CK_ENVIRONMENT=development

Untuk dapat melakukan request ke CloudKit, kita memerlukan API URL nya. Format API URL-nya seperti ini:

const API_URL = `https://api.apple-cloudkit.com/database/1/${container}/${environment}/private`
  • ${container}: value-nya adalah APPLE_CK_CONTAINER.
  • ${environment}: value-nya adalah APPLE_CK_ENVIRONMENT.
  • /private: memberi petunjuk kepada CloudKit jika kita hanya ingin mengakses CloudKit Private Database.

Simpan URL ini dalam sebuah file JavaScript terpisah agar mudah digunakan.

Autentikasi

Autentikasi CloudKit Web Services cukup sederhana. Langkah-langkah nya seperti ini:

Backend (Server)

  1. Login Request: Pertama, kita akan melakukan request ke API /users/current untuk melakukan login. Kita perlu membuat API endpoint di sisi server untuk melakukan request tersebut.

    ./pages/api/auth/login.js
    // API dapat diakses di `/api/auth/login`
    
    export default async function loginHandler(req, res) {
      // Validasi HTTP Method.
      // Hanya GET request yang dapat dieksekusi.
      // Selain itu, return HTTP status 405: Method not allowed.
      if (req.method !== 'GET') {
        res.status(405).json('Method not allowed.')
        return
      }
    
      // Mulai melakukan login request.
      try {
        // Gunakan API_URL yang sudah disimpan sebelumnya
        // dan juga APPLE_CK_API_KEY yang ada di environment variables.
        const loginRequestUrl = `${API_URL}/users/current?ckAPIToken=${process.env.APPLE_CK_API_KEY}`
    
        const fetchResponse = await fetch(loginRequestUrl)
        const json = await fetchResponse.json()
    
        res.json(json)
      } catch (e) {
        res.status(500).json(e)
      }
    }

    Untuk informasi lebih detail tentang API /users/current, lihat dokumentasi lengkapnya.

  2. Auth Callback Handler: Lalu kita juga perlu membuat API endpoint untuk menghandle callback yang diberikan Apple. Sebelum menulis kodenya, kita membutuhkan satu library tambahan yaitu cookies-next untuk memudahkan kita menambahkan, mengubah dan menghapus cookies di server.

    ./pages/api/auth/callback.js
    // API dapat diakses di `/api/auth/callback`
    
    import { setCookie } from 'cookies-next'
    
    export default async function callbackHandler(req, res) {
      // Validasi HTTP Method.
      // Hanya GET request yang dapat dieksekusi.
      // Selain itu, return HTTP status 405: Method not allowed.
      if (req.method !== 'GET') {
        res.status(405).json('Method not allowed.')
        return
      }
    
      // Kita ambil `ckWebAuthToken` dari URL query string.
      const ckWebAuthToken = req.query.ckWebAuthToken
    
      // Jika tidak ada `ckWebAuthToken` di URL query string,
      // return saja HTTP status 500.
      if (!ckWebAuthToken) {
        res.status(500).json('Missing auth token')
        return
      }
    
      // Simpan `ckWebAuthToken` di dalam cookies.
      setCookie('ckWebAuthToken', ckWebAuthToken as string, { req, res })
    
      // Redirect ke halaman utama.
      res.redirect(307, '/')
    }

Frontend (Client)

Sekarang kita akan mengimplementasikan bagian frontend dan menghubungkannya dengan API endpoints yang sudah kita buat. Kita buat dahulu halaman-halaman frontend nya. Sebagai contoh, saya hanya akan membuat 2 halaman saja: / sebagai halaman utama, dan /login.

Selanjutnya, kita akan fokus di halaman login terlebih dahulu. Mari kita hubungkan halaman tersebut dengan API login yang sudah kita buat.

./pages/login.jsx
export default function Login() {
  const handleLogin = async () => {
    try {
      const res = await fetch('/api/auth/login')
      const json = await res.json()

      /**
       * Jika belum login, response dari API tersebut akan seperti ini:
       * {
       *   serverErrorCode: 'AUTHENTICATION_REQUIRED',
       *   redirectUrl: '<url>',
       *   ...
       * }
       *
       * Jika `serverErrorCode` = 'AUTHENTICATION_REQUIRED'
       * Kita redirect pengguna ke halaman login Apple yang disediakan di
       * `redirectUrl`
       */
      if (json.serverErrorCode === 'AUTHENTICATION_REQUIRED') {
        window.location.href = json.redirectURL
        return
      }

      // Tambahkan kode lain untuk menghandle jika sudah login.
      // ...
    } catch (e) {
      console.log(e)
    }
  }

  return (
    <div>
      <h1>Login Page</h1>
      <button onClick={handleLogin}>Login</button>
    </div>
  )
}

Setelah pengguna berhasil login, Apple akan mengarahkan mereka ke API endpoint callback kita (/api/auth/callback). Dari sana, pengguna akan diarahkan kembali ke halaman utama.

Saat ini, kita masih dapat langsung mengakses halaman utama walaupun kita belum login. Untuk itu kita dapat menambahkan proteksi ke halaman tersebut dengan Next.js Middleware.

./middleware.js
// Taruh file ini di root folder project Next.js Anda.

import { NextResponse } from 'next/server'

export function middleware(request) {
  // Cek apakah pengguna sudah login atau belum.
  // Jika belum, redirect ke halaman login.
  if (request.nextUrl.pathname !== '/login') {
    const isTokenExist = request.cookies.has('ckWebAuthToken')

    if (!isTokenExist) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  // Jika sudah login, tapi pengguna mengakses halaman login,
  // redirect ke halaman utama.
  if (request.nextUrl.pathname === '/login') {
    const isTokenExist = request.cookies.has('ckWebAuthToken')

    if (isTokenExist) {
      return NextResponse.redirect(new URL('/', request.url))
    }
  }
}

export const config = {
  matcher: [
    // Sebuah matcher supaya middleware hanya berjalan di halaman client-side saja.
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

Mengakses Data

Setelah berhasil melakukan autentikasi, kita dapat mulai mengakses data CloudKit. Ada beberapa hal yang penting untuk diperhatikan:

  1. Keamanan

    • Semua request ke CloudKit Web Services harus dilakukan dari server-side.
    • Data APPLE_CK_API_KEY yang ada di environment variables tidak boleh digunakan di client-side. Jika Anda menaruhnya di client-side maka Anda membocorkannya ke publik.
    • Data ckWebAuthToken sudah kita simpan di dalam cookies dan dapat diakses di server. Kita tidak perlu mengirimnya dari sisi client.
  2. Implementasi

    • Dengan Next.js, kita dapat menggunakan API Routes (server) untuk menangani request ke CloudKit Web Services dengan aman.
    • Langkah-langkah di bawah ini hanya menunjukan cara dasar melakukan request ke CloudKit Web Services. Anda perlu menambahkan hal-hal lain seperti validasi dan langkah-langkah keamanan lainnya sesuai kebutuhan.

Query Data

Untuk melakukan query data, kita dapat menggunakan API /records/query. Contoh API endpoint-nya seperti ini:

./pages/api/todos/index.js
// API dapat diakses di `/api/todos`

import { getCookie } from 'cookies-next'

export default async function getTodos(req, res) {
  // Validasi HTTP Method.
  // Hanya GET request yang dapat dieksekusi.
  // Selain itu, return HTTP status 405: Method not allowed.
  if (req.method !== 'GET') {
    res.status(405).json('Method not allowed.')
    return
  }

  // Validasi `ckWeabAuthToken`.
  const rawToken = getCookie('ckWebAuthToken', { req, res })

  if (!rawToken) {
    res.status(401).json('Unauthorized')
    return
  }

  const encodeToken = encodeURIComponent(rawToken)

  // Request payload yang akan dikirim.
  const requestPayload = {
    zoneID: { zoneName: 'com.apple.coredata.cloudkit.zone' },
    query: {
      recordType: 'CD_Todo',
      sortBy: [{ fieldName: 'CD_createdAt', ascending: false }],
    },
  }

  try {
    const fetchResponse = await fetch(
      `${API_URL}/records/query?ckAPIToken=${process.env.APPLE_CK_API_KEY}&ckWebAuthToken=${encodeToken}`,
      {
        method: 'POST',
        body: JSON.stringify(requestPayload),
      },
    )
    const json = await fetchResponse.json()

    // Jika ada kesalahan dalam mengambil data,
    // return error kepada client-side.
    if (!fetchResponse.ok) {
      throw json
    }

    // Jika ternyata waktu sesi telah habis,
    // return error kepada client-side.
    // Supaya dapat dilakukan autentikasi ulang.
    if (json.serverErrorCode === 'AUTHENTICATION_REQUIRED') {
      throw json
    }

    if (json.records) {
      // Merapikan data supaya dapat langsung dipakai oleh frontend (client-side)
      const todosToDisplay = json.records
        .filter((record) => !record.deleted)
        .map((record) => ({
          id: record.recordName,
          recordChangeTag: record.recordChangeTag,
          title: record.fields.CD_title.value,
          isCompleted: Boolean(record.fields.CD_isCompleted.value), // raw value: '1' || '0'
        }))

      res.json(todosToDisplay)
      return
    }
  } catch (e) {
    res.status(500).json(e)
  }
}

Beberapa hal penting dalam request payload:

  • zoneID.zoneName

    Field ini wajib diisi. Isi dengan com.apple.coredata.cloudkit.zone untuk mengambil data dari record zone yang sama seperti yang kita gunakan di aplikasi native kita.

  • query

    Field ini wajib diisi. Field ini bentuknya adalah object yang dapat diisi sesuai query yang kita inginkan. Dalam contoh di atas, saya mengambil data dari record type (tabel) CD_Todo dan diurutkan dari CD_createdAt yang terbaru (ascending: false). Untuk detail query request yang lainnya seperti filter (where clause), pelajari di dokumentasi.

Selanjutnya adalah response dari API tersebut. Ada cukup banyak detail-detail yang sebenarnya tidak kita perlukan di tampilan data kita. Sementara data yang kita perlukan hanya 3 data ini:

  • record.recordName

    Apple menggunakan recordName sebagai unique identifier untuk setiap data. Data ini akan kita gunakan untuk menampilkan dan memanipulasi data tersebut.

  • record.recordChangeTag

    Apple menggunakan field ini untuk manajemen perubahan data dalam prinsip "local-first". Kita harus menyertakan tag ini saat melakukan perubahan atau penghapusan data, agar Apple dapat melacak perubahannya.

  • record.fields

    Data ini berisi data utama yang kita simpan. Misalnya, jika kita menyimpan title & isCompleted seperti contoh di atas, kita dapat mengakses kedua informasi tersebut melalui record.fields.

Untuk melihat response lengkap dari API tersebut, lihat di dokumentasi.

Manipulasi Data

Untuk memanipulasi data di CloudKit, kita menggunakan API /records/modify. API ini memungkinkan kita untuk melakukan operasi penambahan, perubahan, dan penghapusan data. Juga, jika dibutuhkan, API ini dapat melakukan operasi bulk.

Ini adalah format dasar yang kita request ke API /records/modify:

const requestPayload = {
  zoneID: { zoneName: 'com.apple.coredata.cloudkit.zone' },
  operations: [
    // ... list of operations
    // ... penambahan, perubahan, penghapusan data
  ],
}

Field operations berbentuk array. Hal ini yang memungkinkan kita untuk melakukan berbagai operasi dalam satu request (bulk). Semua operasi bentuknya akan mirip-mirip seperti format di atas, yang akan membedakan hanya isi dari field operations.

Tambah Data

Untuk menambahkan data baru, kita dapat menaruhnya di dalam field operations seperti ini:

./pages/api/todos/create.js
// API dapat diakses di `/api/todos/create`

export default async function createTodo(req, res) {
  // Validasi HTTP Method.
  // Hanya POST request yang dapat dieksekusi.
  // Selain itu, return HTTP status 405: Method not allowed.
  if (req.method !== 'POST') {
    res.status(405).json('Method not allowed.')
    return
  }

  // Validasi `ckWeabAuthToken`.
  const rawToken = getCookie('ckWebAuthToken', { req, res })

  if (!rawToken) {
    res.status(401).json({ data: null, error: 'Unauthorized' })
    return
  }

  const encodeToken = encodeURIComponent(rawToken)

  // Ambil data dari request body yang dikirimkan dari frontend.
  const data = JSON.parse(req.body)

  const addTodo = {
    operationType: 'create',
    record: {
      recordType: 'CD_Todo',
      fields: {
        CD_title: { value: data.title },
        CD_isCompleted: { value: data.isCompleted },
        CD_createdAt: { value: Date.now() },
        CD_entityName: { value: 'Todo' },
      },
    },
  }

  const requestPayload = {
    zoneID: { zoneName: 'com.apple.coredata.cloudkit.zone' },
    operations: [addTodo],
  }

  try {
    const fetchResponse = await fetch(
      `${API_URL}/records/modify?ckAPIToken=${process.env.APPLE_CK_API_KEY}&ckWebAuthToken=${encodeToken}`,
      {
        method: 'POST',
        body: JSON.stringify(requestPayload),
      },
    )
    const json = await fetchResponse.json()

    if (!fetchResponse.ok) {
      throw json
    }

    res.json(json)
  } catch (e) {
    res.status(500).json(e)
  }
}
  • operationType - Tipe operasi yang diinginkan. Untuk menambah data baru, gunakan create.
  • record.recordType - Nama tabel yang digunakan. Dalam contoh ini saya menggunakan tabel CD_Todo.
  • record.fields - Data yang ingin kita simpan dan harus sesuai dengan schema yang sudah ada di CloudKit.
    • Perhatikan field tambahan: CD_entityName, yang valuenya sama seperti record.recordType namun tanpa prefix CD_, jadi hanya Todo saja.

Pelajari lebih lanjut tentang penambahan data.

Ubah Data

Untuk mengubah data, kita dapat menaruhnya di field operations seperti ini:

./pages/api/todos/update.js
// API dapat diakses di `/api/todos/update`

export default async function updateTodo(req, res) {
  // Validasi HTTP Method.
  // Hanya PUT request yang dapat dieksekusi.
  // Selain itu, return HTTP status 405: Method not allowed.
  if (req.method !== 'PUT') {
    res.status(405).json('Method not allowed.')
    return
  }

  // Validasi `ckWeabAuthToken`.
  const rawToken = getCookie('ckWebAuthToken', { req, res })

  if (!rawToken) {
    res.status(401).json({ data: null, error: 'Unauthorized' })
    return
  }

  const encodeToken = encodeURIComponent(rawToken)

  // Ambil data dari request body yang dikirimkan dari frontend.
  const data = JSON.parse(req.body)

  const updateTodo = {
    operationType: 'update',
    record: {
      recordName: data.id,
      recordChangeTag: data.recordChangeTag,
      fields: {
        CD_title: { value: data.title },
        CD_isCompleted: { value: data.isCompleted ? 1 : 0 },
      },
    },
  }

  const requestPayload = {
    zoneID: { zoneName: 'com.apple.coredata.cloudkit.zone' },
    operations: [updateTodo],
  }

  try {
    const fetchResponse = await fetch(
      `${API_URL}/records/modify?ckAPIToken=${process.env.APPLE_CK_API_KEY}&ckWebAuthToken=${encodeToken}`,
      {
        method: 'POST',
        body: JSON.stringify(requestPayload),
      },
    )
    const json = await fetchResponse.json()

    if (!fetchResponse.ok) {
      throw json
    }

    res.json(json)
  } catch (e) {
    res.status(500).json(e)
  }
}

Proses perubahan data mirip dengan penambahan data, namun dengan beberapa perbedaan penting:

  • operationType: 'update' - Gunakan tipe operasi 'update' untuk mengubah data yang sudah ada.
  • record.recordName - Sertakan ID yang ingin diubah.
  • record.recordChangeTag - Sertakan 'recordChangeTag' dari data yang ingin diubah.
  • record.fields - Hanya sertakan field-field yang ingin diubah. Tidak perlu menyertakan seluruh data jika hanya sebagian yang diubah.

Pelajari lebih lanjut tentang perubahan data.

Hapus Data

Untuk menghapus data, kita bisa menaruhnya di field operations seperti ini:

./pages/api/todos/delete.js
// API dapat diakses di `/api/todos/delete`

export default async function deleteTodo(req, res) {
  // Validasi HTTP Method.
  // Hanya DELETE request yang dapat dieksekusi.
  // Selain itu, return HTTP status 405: Method not allowed.
  if (req.method !== 'DELETE') {
    res.status(405).json('Method not allowed.')
    return
  }

  // Validasi `ckWeabAuthToken`.
  const rawToken = getCookie('ckWebAuthToken', { req, res })

  if (!rawToken) {
    res.status(401).json({ data: null, error: 'Unauthorized' })
    return
  }

  const encodeToken = encodeURIComponent(rawToken)

  // Ambil data dari request body yang dikirimkan dari frontend.
  const data = JSON.parse(req.body)

  const deleteTodo = {
    operationType: 'delete',
    record: {
      recordName: data.id,
      recordChangeTag: data.recordChangeTag,
    },
  }

  const requestPayload = {
    zoneID: { zoneName: 'com.apple.coredata.cloudkit.zone' },
    operations: [deleteTodo],
  }

  try {
    const fetchResponse = await fetch(
      `${API_URL}/records/modify?ckAPIToken=${process.env.APPLE_CK_API_KEY}&ckWebAuthToken=${encodeToken}`,
      {
        method: 'POST',
        body: JSON.stringify(requestPayload),
      },
    )
    const json = await fetchResponse.json()

    if (!fetchResponse.ok) {
      throw json
    }

    res.json(json)
  } catch (e) {
    res.status(500).json(e)
  }
}

Sama seperti sebelumnya, kode yang berubah hanya yang ada di dalam operations saja. Informasi yang dikirimkan lebih sederhana:

  • operationType: 'delete' - Gunakan tipe operasi 'delete' untuk menghapus data.
  • record.recordName - Sertakan ID yang akan dihapus.
  • record.recordChangeTag - Sertakan 'recordChangeTag' dari data yang ingin dihapus.

Pelajari lebih lanjut tentang penghapusan data.

Menggunakan Data di Frontend

Setelah mengimplementasikan semua API untuk operasi CRUD (Create, Read, Update, Delete), kita dapat menggunakannya di frontend. Saya hanya akan menuliskan bentuk-bentuk contoh saja, supaya Anda mendapatkan gambaran bagaimana cara menggunakannya.

Catatan Penting: Contoh-contoh berikut hanya untuk ilustrasi dan tidak disarankan untuk versi production. Saya menyarankan Anda untuk menggunakan library seperti @tanstack/react-query atau swr untuk mempermudah mengelola asynchronous API calls.

Query Data

Untuk menampilkan data, gunakan API /api/todos yang sudah kita buat sebelumnya. Misalnya seperti ini:

./pages/index.jsx
import { useQuery } from '@tanstack/react-query'

export default function MainPage() {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await fetch('/api/todos')
      const json = await res.json()

      if (!res.ok) throw json

      return json
    },
  })

  return (
    <div>
      {isLoading ? (
        <div>Loading...</div>
      ) : data.length > 0 ? (
        <div>
          {data.map((todo: any, index: number) => (
            <TodoItem
              key={todo.id}
              isFirstItem={index === 0}
              isLastItem={index === data.length - 1}
              todo={todo}
            />
          ))}
        </div>
      ) : null}
    </div>
  )
}

Tambah Data

Untuk menambahkan data, gunakan API /api/todos/create yang sudah kita buat sebelumnya. Misalnya seperti ini:

import { useMutation } from '@tanstack/react-query'

export default function AddTodoForm() {
  const createTodoMutation = useMutation({
    mutationFn: async (data) => {
      const res = await fetch('/api/todos/create', {
        method: 'POST',
        body: JSON.stringify(data),
      })
      const json = await res.json()

      if (!res.ok) throw json

      return json
    },
    onSuccess: (data) => {
      // ... Refresh Data ...

      // Reset form
      formRef.current?.reset()
    },
    onError: (error) => console.log(error),
  })

  const onSubmit = async (e) => {
    e.preventDefault()

    const formData = new FormData(e.currentTarget)
    const data = Object.fromEntries(formData)

    if (!data.title) return

    const formattedData = { ...data, isCompleted: 0 }

    createTodoMutation.mutate(formattedData)
  }

  return (
    <div>
      <form
        ref={formRef}
        className="mb-4 flex gap-2 rounded-lg border bg-white px-3"
        onSubmit={onSubmit}
      >
        <div className="flex-1">
          <input
            required
            name="title"
            placeholder="Todo Title"
            className="w-full py-2 focus:outline-none"
            disabled={createMutation.isPending}
          />
        </div>

        <button
          type="submit"
          className="shrink-0 px-2 font-semibold text-[#007AFF] active:opacity-50 disabled:text-neutral-500"
          disabled={createMutation.isPending}
        >
          {createMutation.isPending ? 'Saving...' : 'Save'}
        </button>
      </form>
    </div>
  )
}

Ubah & Hapus Data

  • Untuk mengubah data, gunakan API /api/todos/update yang sudah kita buat sebelumnya. Misalnya seperti ini:

    import { useMutation } from '@tanstack/react-query'
    
    export default function UpdateTodo({ todo }) {
      const updateMutation = useMutation({
        mutationFn: async (data) => {
          const res = await fetch(`/api/todos/update`, {
            method: 'PUT',
            body: JSON.stringify(data),
          })
          const json = await res.json()
    
          if (!res.ok) throw json
    
          return json
        },
        onSuccess: (data) => {
          // ... Refresh Data ...
        },
        onError: (error) => console.log(error),
      })
    
      const onUpdate = (e) => {
        e.preventDefault()
    
        const formData = new FormData(e.currentTarget)
        const data = Object.fromEntries(formData)
    
        if (!data.title) return
    
        const formattedData = {
          ...data,
          id: todo.id,
          recordChangeTag: todo.recordChangeTag,
        }
    
        updateMutation.mutate(formattedData)
      }
    
      return <form onSubmit={onUpdate}>...</form>
    }
  • Untuk menghapus data, gunakan API /api/todos/delete yang sudah kita buat sebelumnya. Misalnya seperti ini:

    import { useMutation } from '@tanstack/react-query'
    
    export default function DeleteTodo({ todo }) {
      const deleteMutation = useMutation({
        mutationFn: async (data) => {
          const res = await fetch(`/api/todos/delete`, {
            method: 'DELETE',
            body: JSON.stringify(data),
          })
          const json = await res.json()
    
          if (!res.ok) throw json
    
          return json
        },
        onSuccess: (data) => {
          // ... Refresh Data ...
        },
        onError: (error) => console.log(error),
      })
    
      const onDelete = () => {
        deleteMutation.mutate({
          id: todo.id,
          recordChangeTag: todo.recordChangeTag,
        })
      }
    
      return <button onClick={onDelete}>...</button>
    }

Perlu diingat bahwa contoh-contoh ini adalah implementasi dasar. Dalam aplikasi production, Anda perlu menambahkan penanganan error, loading states, dan optimisasi performa. Penggunaan library state-management dan data-fetching dapat sangat membantu dalam mengelola kompleksitas ini.

Penutup

Mengintegrasikan CloudKit Web Services ke dalam aplikasi web ternyata tidak serumit yang saya bayangkan. Kunci keberhasilannya ada pada ketekunan dan kegigihan dalam menelusuri dan memahami dokumentasi yang disediakan oleh Apple.

Tulisan ini merupakan hasil pengembangan dari apa yang saya pelajari dari dokumentasi resmi Apple. Bagi Anda yang serius ingin mengembangkan aplikasi web dengan CloudKit, saya sangat menyarankan untuk meluangkan waktu lebih banyak untuk mempelajari dokumentasinya secara menyeluruh.

Untuk referensi implementasi yang lebih lengkap dan menyeluruh, saya telah menyediakan kodenya di GitHub. Namun, perlu diingat bahwa kode tersebut hanya untuk environment development saja, dan belum diuji secara menyeluruh untuk environment production. Jika Anda berniat menggunakannya, lakukan dengan bijak — tambahkan fitur keamanan dan pengujian tambahan. Singkatnya, Do With Your Own Risk (DWYOR).

Jika Anda memiliki pertanyaan atau membutuhkan penjelasan lebih lanjut, jangan ragu untuk menghubungi saya di Twitter. Selamat mencoba!