ขยาย Realtime Database ด้วย Cloud Functions


Cloud Functions ช่วยให้คุณจัดการเหตุการณ์ใน Firebase Realtime Database ได้โดยไม่ต้องอัปเดตโค้ดไคลเอ็นต์ Cloud Functions ช่วยให้คุณดําเนินการกับ Realtime Database ด้วยสิทธิ์ระดับผู้ดูแลระบบแบบเต็ม และช่วยให้มั่นใจได้ว่าระบบจะประมวลผลการเปลี่ยนแปลงแต่ละรายการใน Realtime Database แยกกัน คุณทําการเปลี่ยนแปลง Firebase Realtime Database ได้ผ่าน DataSnapshot หรือผ่าน Admin SDK

ในวงจรปกติ ฟังก์ชัน Firebase Realtime Database จะทําสิ่งต่อไปนี้

  1. รอการเปลี่ยนแปลงในRealtime Databaseตําแหน่งหนึ่งๆ
  2. ทริกเกอร์เมื่อเกิดเหตุการณ์และดําเนินการ (ดูฉันทําอะไรได้บ้างกับ Cloud Functions ดูตัวอย่าง Use Case)
  3. รับออบเจ็กต์ข้อมูลซึ่งมีสแนปชอตของข้อมูลที่จัดเก็บในเอกสารที่ระบุ

ทริกเกอร์ฟังก์ชัน Realtime Database

สร้างฟังก์ชันใหม่สําหรับเหตุการณ์ Realtime Database ด้วย functions.database หากต้องการควบคุมเวลาที่ฟังก์ชันจะทริกเกอร์ ให้ระบุตัวแฮนเดิลเหตุการณ์รายการใดรายการหนึ่ง และระบุเส้นทาง Realtime Database ที่ฟังเหตุการณ์

ตั้งค่าเครื่องจัดการเหตุการณ์

ฟังก์ชันช่วยให้คุณจัดการเหตุการณ์ Realtime Database ในระดับความเฉพาะเจาะจง 2 ระดับได้ โดยคุณสามารถรอฟังเฉพาะเหตุการณ์การสร้าง การอัปเดต หรือการลบ หรือจะรอฟังการเปลี่ยนแปลงทุกประเภทในเส้นทางก็ได้ Cloud Functions รองรับตัวแฮนเดิลเหตุการณ์ต่อไปนี้สําหรับ Realtime Database

  • onWrite() ซึ่งจะทริกเกอร์เมื่อสร้าง อัปเดต หรือลบข้อมูลใน Realtime Database
  • onCreate() ซึ่งจะทริกเกอร์เมื่อมีการสร้างข้อมูลใหม่ใน Realtime Database
  • onUpdate() ซึ่งจะทริกเกอร์เมื่อมีการอัปเดตข้อมูลใน Realtime Database
  • onDelete() ซึ่งจะทริกเกอร์เมื่อมีการลบข้อมูลจาก Realtime Database

ระบุอินสแตนซ์และเส้นทาง

หากต้องการควบคุมเวลาและตำแหน่งที่ฟังก์ชันควรทริกเกอร์ ให้เรียกใช้ ref(path) เพื่อระบุเส้นทาง และระบุอินสแตนซ์ Realtime Database (ไม่บังคับ) ด้วย instance('INSTANCE_NAME') หากคุณไม่ได้ระบุอินสแตนซ์ ฟังก์ชันจะติดตั้งใช้งานในอินสแตนซ์ Realtime Database เริ่มต้นของโปรเจ็กต์ Firebase ตัวอย่างเช่น

  • อินสแตนซ์ Realtime Database เริ่มต้น: functions.database.ref('/foo/bar')
  • อินสแตนซ์ชื่อ "my-app-db-2": functions.database.instance('my-app-db-2').ref('/foo/bar')

วิธีการเหล่านี้จะกำหนดให้ฟังก์ชันจัดการการเขียนในบางเส้นทางภายในอินสแตนซ์ Realtime Database ข้อกําหนดของเส้นทางจะจับคู่การเขียนทั้งหมดที่สัมผัสเส้นทาง รวมถึงการเขียนที่เกิดขึ้นที่ใดก็ได้ด้านล่าง หากคุณตั้งค่าเส้นทางสำหรับฟังก์ชันเป็น /foo/bar ระบบจะจับคู่เหตุการณ์ที่ตำแหน่งทั้ง 2 ตำแหน่งนี้

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

ไม่ว่าในกรณีใด Firebase จะตีความว่าเหตุการณ์เกิดขึ้นที่ /foo/bar และข้อมูลเหตุการณ์จะรวมข้อมูลเก่าและใหม่ ณ /foo/bar หากข้อมูลเหตุการณ์มีขนาดใหญ่ ให้พิจารณาใช้ฟังก์ชันหลายรายการในเส้นทางที่ลึกขึ้นแทนการใช้ฟังก์ชันเดียวใกล้กับรูทของฐานข้อมูล ขอเฉพาะข้อมูลที่ระดับลึกที่สุดเท่าที่จะเป็นไปได้เพื่อให้ได้ประสิทธิภาพที่ดีที่สุด

คุณสามารถระบุคอมโพเนนต์เส้นทางเป็นไวลด์การ์ดได้โดยใส่วงเล็บปีกกาล้อมไว้ ref('foo/{bar}') จะจับคู่กับรายการย่อยของ /foo ค่าของคอมโพเนนต์เส้นทางไวลด์การ์ดเหล่านี้จะอยู่ในออบเจ็กต์ EventContext.params ของฟังก์ชัน ในตัวอย่างนี้ ค่าจะพร้อมใช้งานเป็น context.params.bar

เส้นทางที่มีไวลด์การ์ดจะจับคู่กับเหตุการณ์หลายรายการจากการเขียนครั้งเดียวได้ แทรกของ

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

จับคู่เส้นทาง "/foo/{bar}" 2 ครั้ง ได้แก่ 1 ครั้งกับ "hello": "world" และอีก 1 ครั้งกับ "firebase": "functions"

จัดการข้อมูลเหตุการณ์

เมื่อจัดการเหตุการณ์ Realtime Database ออบเจ็กต์ข้อมูลที่แสดงผลคือ DataSnapshot สําหรับเหตุการณ์ onWrite หรือ onUpdate พารามิเตอร์แรกคือออบเจ็กต์ Change ที่มีภาพรวม 2 รายการซึ่งแสดงสถานะข้อมูลก่อนและหลังเหตุการณ์เรียกให้แสดง สําหรับเหตุการณ์ onCreate และ onDelete ออบเจ็กต์ข้อมูลที่แสดงผลคือสแนปชอตของข้อมูลที่สร้างขึ้นหรือลบ

ในตัวอย่างนี้ ฟังก์ชันจะดึงข้อมูลสแนปชอตสำหรับเส้นทางที่ระบุ แปลงสตริงที่ตำแหน่งนั้นให้เป็นอักษรตัวพิมพ์ใหญ่ และเขียนสตริงที่แก้ไขแล้วลงในฐานข้อมูล

// 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);
    });

การเข้าถึงข้อมูลการตรวจสอบสิทธิ์ของผู้ใช้

จาก EventContext.auth และ EventContext.authType คุณสามารถเข้าถึงข้อมูลผู้ใช้ รวมถึงสิทธิ์ของผู้ใช้ที่เรียกใช้ฟังก์ชัน ซึ่งอาจมีประโยชน์ในการบังคับใช้กฎความปลอดภัย ซึ่งจะช่วยให้ฟังก์ชันดำเนินการต่างๆ ให้เสร็จสมบูรณ์ตามระดับสิทธิ์ของผู้ใช้

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);
      }
    });

นอกจากนี้ คุณยังใช้ประโยชน์จากข้อมูลการตรวจสอบสิทธิ์ของผู้ใช้เพื่อ "แอบอ้างเป็น" ผู้ใช้ และดําเนินการเขียนในนามของผู้ใช้ได้ด้วย อย่าลืมลบอินสแตนซ์แอปตามที่แสดงด้านล่างเพื่อป้องกันปัญหาการเรียกใช้พร้อมกัน

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));
      });
    });

การอ่านค่าก่อนหน้า

ออบเจ็กต์ Change มีพร็อพเพอร์ตี้ before ที่ให้คุณตรวจสอบสิ่งที่บันทึกไว้ใน Realtime Database ก่อนเหตุการณ์ พร็อพเพอร์ตี้ before จะแสดงผล DataSnapshot โดยที่เมธอดทั้งหมด (เช่น val() และ exists()) จะอ้างอิงค่าก่อนหน้า คุณสามารถอ่านค่าใหม่อีกครั้งได้โดยใช้ DataSnapshot เดิมหรืออ่านพร็อพเพอร์ตี้ after พร็อพเพอร์ตี้นี้ใน Change ใดก็ตามคือ DataSnapshot อีกรายการหนึ่งที่แสดงสถานะข้อมูลหลังจากเหตุการณ์เกิดขึ้น

เช่น คุณสามารถใช้พร็อพเพอร์ตี้ before เพื่อให้แน่ใจว่าฟังก์ชันจะเปลี่ยนเฉพาะข้อความเป็นอักษรตัวพิมพ์ใหญ่เมื่อสร้างครั้งแรกเท่านั้น ดังนี้

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);
    });