Melengkapi Realtime Database dengan Cloud Functions


Dengan Cloud Functions, Anda dapat menangani peristiwa di Firebase Realtime Database tanpa harus memperbarui kode klien. Cloud Functions dapat Anda gunakan untuk menjalankan operasi Realtime Database dengan hak istimewa administratif penuh, dan memastikan setiap perubahan pada Realtime Database diproses secara terpisah. Anda dapat membuat perubahan pada Firebase Realtime Database melalui DataSnapshot atau melalui Admin SDK.

Dalam siklus proses umum, fungsi Firebase Realtime Database melakukan hal-hal berikut:

  1. Menunggu perubahan pada lokasi Realtime Database tertentu.
  2. Terpicu ketika suatu peristiwa terjadi dan menjalankan tugasnya (untuk mengetahui contoh kasus penggunaan, baca artikel Apa yang bisa dilakukan dengan Cloud Functions?).
  3. Menerima objek data yang berisi snapshot data yang disimpan dalam dokumen yang ditentukan.

Memicu fungsi Realtime Database

Membuat fungsi baru untuk peristiwa Realtime Database dengan functions.database. Untuk mengontrol kapan fungsi terpicu, tentukan salah satu pengendali peristiwa dan tentukan jalur Realtime Database tempat peristiwa akan diproses.

Menetapkan pengendali peristiwa

Dengan Functions, Anda dapat menangani peristiwa Realtime Database di dua tingkat kekhususan; Anda dapat hanya memproses peristiwa pembuatan, pembaruan, atau penghapusan secara khusus, atau Anda dapat memproses perubahan apa pun pada jalur. Cloud Functions mendukung pengendali peristiwa ini untuk Realtime Database:

  • onWrite(), yang terpicu saat data dibuat, diperbarui, atau dihapus di Realtime Database.
  • onCreate(), yang terpicu saat data baru dibuat di Realtime Database.
  • onUpdate(), yang terpicu saat data diperbarui di Realtime Database.
  • onDelete(), yang terpicu saat data dihapus dari Realtime Database.

Menentukan instance dan jalur

Untuk mengontrol kapan dan di mana fungsi Anda harus terpicu, panggil ref(path) untuk menentukan jalur, dan tentukan instance Realtime Database dengan instance('INSTANCE_NAME'). Jika Anda tidak menentukan instance, fungsi tersebut akan di-deploy ke instance Realtime Database default untuk project Firebase. Misalnya:

  • Instance Realtime Database default: functions.database.ref('/foo/bar')
  • Instance dengan nama "my-app-db-2": functions.database.instance('my-app-db-2').ref('/foo/bar')

Metode ini akan mengarahkan fungsi Anda untuk menangani penulisan di jalur tertentu dalam instance Realtime Database. Spesifikasi jalur cocok dengan semua penulisan yang menyentuh suatu jalur, termasuk penulisan yang terjadi di bawahnya. Jika Anda menetapkan /foo/bar sebagai jalur fungsi, jalur tersebut akan cocok dengan peristiwa di kedua lokasi ini:

 /foo/bar
 /foo/bar/baz/really/deep/path

Bagaimanapun juga, Firebase akan menafsirkan bahwa peristiwa terjadi di /foo/bar, dan data peristiwa menyertakan data lama dan baru di /foo/bar. Jika data peristiwa kemungkinan berukuran besar, sebaiknya gunakan beberapa fungsi di jalur yang lebih dalam, bukan fungsi tunggal di dekat root database Anda. Untuk mendapatkan performa terbaik, hanya minta data di level sedalam mungkin.

Anda dapat menentukan komponen jalur sebagai karakter pengganti dengan mengapitnya menggunakan tanda kurung kurawal; ref('foo/{bar}') cocok dengan turunan apa pun dari /foo. Nilai komponen jalur karakter pengganti ini tersedia dalam objek EventContext.params di fungsi Anda. Dalam contoh ini, nilainya tersedia sebagai context.params.bar.

Jalur dengan karakter pengganti dapat cocok dengan beberapa peristiwa dari satu penulisan. Sisipan

{
  "foo": {
    "hello": "world",
    "firebase": "functions"
  }
}

cocok dengan jalur "/foo/{bar}" dua kali: sekali dengan "hello": "world" dan sekali lagi dengan "firebase": "functions".

Menangani data peristiwa

Saat menangani peristiwa Realtime Database, objek data yang ditampilkan adalah DataSnapshot. Untuk peristiwa onWrite atau onUpdate, parameter pertama adalah objek Change yang berisi dua snapshot yang mewakili status data sebelum dan setelah peristiwa pemicu. Untuk peristiwa onCreate dan onDelete, objek data yang ditampilkan adalah snapshot dari data yang dibuat atau dihapus.

Dalam contoh ini, fungsi mengambil snapshot untuk jalur yang ditentukan, mengubah string di lokasi tersebut menjadi huruf kapital, dan menulis string yang telah diubah tersebut ke database:

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      functions.logger.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

Mengakses informasi autentikasi pengguna

Dari EventContext.auth dan EventContext.authType, Anda dapat mengakses informasi pengguna, termasuk izin, untuk pengguna yang memicu suatu fungsi. Tindakan ini dapat digunakan untuk menerapkan aturan keamanan sehingga fungsi Anda dapat menyelesaikan berbagai operasi sesuai dengan tingkat izin pengguna:

const functions = require('firebase-functions/v1');
const admin = require('firebase-admin');

exports.simpleDbFunction = functions.database.ref('/path')
    .onCreate((snap, context) => {
      if (context.authType === 'ADMIN') {
        // do something
      } else if (context.authType === 'USER') {
        console.log(snap.val(), 'written by', context.auth.uid);
      }
    });

Anda juga dapat memanfaatkan informasi autentikasi pengguna untuk "meniru identitas" pengguna dan melakukan operasi tulis atas nama pengguna tersebut. Pastikan untuk menghapus instance aplikasi seperti yang ditunjukkan di bawah ini untuk mencegah masalah serentak:

exports.impersonateMakeUpperCase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snap, context) => {
      const appOptions = JSON.parse(process.env.FIREBASE_CONFIG);
      appOptions.databaseAuthVariableOverride = context.auth;
      const app = admin.initializeApp(appOptions, 'app');
      const uppercase = snap.val().toUpperCase();
      const ref = snap.ref.parent.child('uppercase');

      const deleteApp = () => app.delete().catch(() => null);

      return app.database().ref(ref).set(uppercase).then(res => {
        // Deleting the app is necessary for preventing concurrency leaks
        return deleteApp().then(() => res);
      }).catch(err => {
        return deleteApp().then(() => Promise.reject(err));
      });
    });

Membaca nilai sebelumnya

Objek Change memiliki properti before yang dapat digunakan untuk memeriksa apa saja yang telah disimpan ke Realtime Database sebelum peristiwa. Properti before menampilkan DataSnapshot yang semua metodenya (misalnya, val() dan exists()) mengacu ke nilai sebelumnya. Anda dapat membaca kembali nilai baru menggunakan DataSnapshot yang asli atau membaca properti after. Properti ini di setiap Change adalah DataSnapshot lain yang mewakili status data setelah peristiwa terjadi.

Misalnya, properti before dapat digunakan untuk memastikan bahwa fungsi hanya mengubah teks menjadi huruf kapital ketika pertama kali dibuat:

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onWrite((change, context) => {
      // Only edit data when it is first created.
      if (change.before.exists()) {
        return null;
      }
      // Exit when the data is deleted.
      if (!change.after.exists()) {
        return null;
      }
      // Grab the current value of what was written to the Realtime Database.
      const original = change.after.val();
      console.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return change.after.ref.parent.child('uppercase').set(uppercase);
    });