Rozszerzanie bazy danych czasu rzeczywistego za pomocą Cloud Functions


Dzięki Cloud Functions możesz obsługiwać zdarzenia w Firebase Realtime Database bez konieczności aktualizowania kodu klienta. Cloud Functions umożliwia wykonywanie operacji Realtime Database z pełnymi uprawnieniami administracyjnymi i zapewnia, że każda zmiana w Realtime Database jest przetwarzana osobno. Zmiany w Firebase Realtime Database możesz wprowadzać za pomocą interfejsu DataSnapshot lub pakietu Admin SDK.

W typowym cyklu życia funkcja Firebase Realtime Database wykonuje te czynności:

  1. Czeka na zmiany w konkretnej lokalizacji Realtime Database.
  2. Aktywuje się, gdy nastąpi zdarzenie, i wykonuje swoje zadania (patrz Co mogę zrobić za pomocą Cloud Functions? (przykłady zastosowań).
  3. Otrzymuje obiekt danych zawierający zrzut danych przechowywanych w określonym dokumencie.

Aktywowanie funkcji Realtime Database

tworzyć nowe funkcje dla zdarzeń Realtime Database za pomocą functions.database. Aby kontrolować, kiedy funkcja się aktywuje, określ jeden z modułów obsługi zdarzeń i określ ścieżkę Realtime Database, w której będzie nasłuchiwać zdarzeń.

Konfigurowanie modułu obsługi zdarzenia

Funkcje umożliwiają obsługę zdarzeń Realtime Database na 2 poziomach dokładności. Możesz nasłuchiwać tylko zdarzeń tworzenia, aktualizowania lub usuwania bądź nasłuchiwać dowolnej zmiany ścieżki. Cloud Functions obsługuje te moduły obsługi zdarzeń w przypadku Realtime Database:

  • onWrite(), który uruchamia się, gdy dane są tworzone, aktualizowane lub usuwane w Realtime Database.
  • onCreate(), która jest uruchaniana, gdy w tabeli Realtime Database są tworzone nowe dane.
  • onUpdate(), które jest wywoływane, gdy dane są aktualizowane w tabeli Realtime Database.
  • onDelete(), który uruchamia się po usunięciu danych z tabeli Realtime Database.

Określ instancję i ścieżkę

Aby określić, kiedy i gdzie ma być wywoływana funkcja, użyj funkcji ref(path), aby podać ścieżkę, i opcjonalnie użyj funkcji Realtime Database, aby podać instancję funkcji instance('INSTANCE_NAME'). Jeśli nie określisz instancji, funkcja zostanie wdrożona do domyślnej instancji Realtime Database w projekcie Firebase. Przykład:

  • Domyślna instancja Realtime Database: functions.database.ref('/foo/bar')
  • Instancja o nazwie „my-app-db-2”: functions.database.instance('my-app-db-2').ref('/foo/bar')

Te metody kierują funkcję do obsługi zapisu w określonym miejscu w obiekcie Realtime Database. Specyfikacje ścieżek pasują do wszystkich operacji zapisu, które dotyczą ścieżki, w tym operacji zapisu, które występują w dowolnym miejscu poniżej. Jeśli ścieżkę funkcji ustawisz jako /foo/bar, funkcja będzie dopasowywać zdarzenia w obu tych lokalizacjach:

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

W obu przypadkach Firebase interpretuje, że zdarzenie występuje w czasie /foo/bar, a dane zdarzenia obejmują stare i nowe dane w czasie /foo/bar. Jeśli dane zdarzenia mogą być duże, rozważ użycie wielu funkcji na głębszych ścieżkach zamiast jednej funkcji w pobliżu katalogu głównego bazy danych. Aby uzyskać najlepszą wydajność, żądaj danych tylko na możliwie najgłębszym poziomie.

Możesz określić element ścieżki jako symbol wieloznaczny, otaczając go nawiasami klamrowymi. ref('foo/{bar}') pasuje do dowolnego elementu podrzędnego elementu /foo. Wartości tych komponentów ścieżki symbolu wieloznacznego są dostępne w obiekcie EventContext.params funkcji. W tym przykładzie wartość jest dostępna jako context.params.bar.

Ścieżki z symbolami wieloznacznymi mogą pasować do wielu zdarzeń z jednego zapisu. Wstawka

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

pasuje do ścieżki "/foo/{bar}" dwukrotnie: raz z "hello": "world" i ponownie z "firebase": "functions".

Obsługa danych zdarzenia

W przypadku obsługi zdarzenia Realtime Database zwracanym obiektem danych jest DataSnapshot. W przypadku zdarzeń onWrite lub onUpdate pierwszy parametr to obiekt Change zawierający 2 migawki, które reprezentują stan danych przed i po wywołaniu zdarzenia. W przypadku zdarzeń onCreate i onDelete zwracany obiekt danych to zrzut danych utworzonych lub usuniętych.

W tym przykładzie funkcja pobiera zrzut określonych danych w ścieżce, zamienia ciąg znaków w tym miejscu na wielkie litery i zapisuje zmodyfikowany ciąg znaków w bazie danych:

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

Uzyskiwanie dostępu do informacji uwierzytelniających użytkowników

W usługach EventContext.auth i EventContext.authType możesz uzyskać dostęp do informacji o użytkowniku, który uruchomił funkcję, w tym do jego uprawnień. Może to być przydatne do egzekwowania reguł zabezpieczeń, ponieważ umożliwia funkcji wykonywania różnych operacji w zależności od poziomu uprawnień użytkownika:

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

Możesz też wykorzystać informacje uwierzytelniające użytkownika, aby „udawać” tego użytkownika i wykonywać operacje zapisu w jego imieniu. Aby uniknąć problemów z jednoczestronnością, usuń instancję aplikacji w ten sposób:

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

Odczytywanie poprzedniej wartości

Obiekt Change ma właściwość before, która umożliwia sprawdzenie, co zostało zapisane w Realtime Database przed zdarzeniem. Właściwość before zwraca wartość DataSnapshot, gdzie wszystkie metody (np. val() i exists()) odwołują się do poprzedniej wartości. Możesz ją odczytać ponownie, używając pierwotnego elementu DataSnapshot lub właściwości after. Ta właściwość w przypadku dowolnego Change to kolejna DataSnapshot, która reprezentuje stan danych po wystąpieniu zdarzenia.

Na przykład za pomocą właściwości before możesz się upewnić, że funkcja będzie zamieniać tekst na wielkie litery tylko przy pierwszym utworzeniu:

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