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

הפעלת פונקציית 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 שמכיל שני צילומי מצב שמייצגים את מצב הנתונים לפני ואחרי האירוע שהפעיל אותם. באירועים מסוג 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);
    });