טריגרים של מסד נתונים בזמן אמת


בעזרת 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? לדוגמה, תרחישים לדוגמה).
  3. מקבל אובייקט נתונים שמכיל תמונת מצב של הנתונים שמאוחסנים במסמך שצוין.

הפעלת פונקציית Realtime Database

יוצרים פונקציות חדשות לאירועי Realtime Database באמצעות functions.database. כדי לקבוע מתי הפונקציה מופעלת, מציינים את אחד מהגורמים המטפלים באירועים ואת הנתיב Realtime Database שבו הוא יקשיב לאירועים.

הגדרת הגורם המטפל באירוע

פונקציות מאפשרות לטפל באירועי Realtime Database בשתי רמות ספציפיות: אפשר להאזין רק לאירועי יצירה, עדכון או מחיקה, או להאזין לכל שינוי מסוג כלשהו בנתיב. ב-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, היא תתאים לאירועים בשני המיקומים האלה:

 /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}" פעמיים: פעם אחת עם "hello": "world" ופעם שנייה עם "firebase": "functions".

טיפול בנתוני אירועים

כשמטפלים באירוע Realtime Database, אובייקט הנתונים המוחזר הוא DataSnapshot. לאירועים onWrite או onUpdate, הפרמטר הראשון הוא אובייקט Change שמכיל שתי קובצי snapshot שמייצגים את מצב הנתונים לפני האירוע המפעיל ואחריו. באירועים מסוג onCreate ו-onDelete, אובייקט הנתונים המוחזר הוא קובץ snapshot של הנתונים שנוצרו או נמחקו.

בדוגמה הזו, הפונקציה מאחזרת את קובץ ה-snapshot של הנתיב שצוין, ממירה את המחרוזת במיקום הזה לאותיות רישיות וכותבת את המחרוזת שהשתנתה במסד הנתונים:

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