Estendere Realtime Database con Cloud Functions


Con Cloud Functions, puoi gestire gli eventi in Firebase Realtime Database senza dover aggiornare il codice client. Cloud Functions ti consente di eseguire operazioni Realtime Database con privilegi amministrativi completi e garantisce che ogni modifica a Realtime Database venga elaborata singolarmente. Puoi apportare modifiche a Firebase Realtime Database tramite DataSnapshot o tramite l'SDK Admin.

In un ciclo di vita tipico, una funzione Firebase Realtime Database esegue le seguenti operazioni:

  1. Attende modifiche a una determinata posizione Realtime Database.
  2. Si attiva quando si verifica un evento ed esegue le relative attività (vedi Che cosa posso fare con Cloud Functions? per esempi di casi d'uso).
  3. Riceve un oggetto dati contenente un'istantanea dei dati archiviati nel documento specificato.

Attiva una funzione Realtime Database

Crea nuove funzioni per gli eventi Realtime Database con functions.database. Per controllare quando viene attivata la funzione, specifica uno dei gestori di eventi e il percorso Realtime Database in cui verranno ascoltati gli eventi.

Imposta il gestore di eventi

Le funzioni ti consentono di gestire gli eventi Realtime Database a due livelli di specificità: puoi ascoltare specificamente solo gli eventi di creazione, aggiornamento o cancellazione oppure puoi ascoltare qualsiasi modifica di qualsiasi tipo a un percorso. Cloud Functions supporta questi gestori eventi per Realtime Database:

  • onWrite(), che si attiva quando i dati vengono creati, aggiornati o eliminati in Realtime Database.
  • onCreate(), che si attiva quando vengono creati nuovi dati in Realtime Database.
  • onUpdate(), che si attiva quando i dati vengono aggiornati in Realtime Database.
  • onDelete(), che si attiva quando i dati vengono eliminati da Realtime Database .

Specifica l'istanza e il percorso

Per controllare quando e dove deve essere attivata la funzione, chiama ref(path) per specificare un percorso e, facoltativamente, un'istanza Realtime Database con instance('INSTANCE_NAME'). Se non specifichi un'istanza, la funzione viene dispiattata nell'istanza Realtime Database predefinita per il progetto Firebase. Ad esempio:

  • Istanza Realtime Database predefinita: functions.database.ref('/foo/bar')
  • Istanza denominata "my-app-db-2": functions.database.instance('my-app-db-2').ref('/foo/bar')

Questi metodi indicano alla tua funzione di gestire le scritture in un determinato percorso all'interno dell'istanza Realtime Database. Le specifiche del percorso corrispondono a tutte le scritture che toccano un percorso, incluse quelle che si verificano al di sotto. Se imposti il percorso per la funzione come /foo/bar, corrisponde agli eventi in entrambe le posizioni:

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

In entrambi i casi, Firebase interpreta che l'evento si verifica in /foo/bar e i dati dell'evento includono i dati vecchi e nuovi in /foo/bar. Se i dati degli eventi potrebbero essere di grandi dimensioni, valuta la possibilità di utilizzare più funzioni in percorsi più profondi invece di una singola funzione vicino alla directory principale del database. Per ottenere le migliori prestazioni, richiedi solo i dati al livello più granulare possibile.

Puoi specificare un componente del percorso come carattere jolly racchiudendolo tra parentesi graffe. ref('foo/{bar}') corrisponde a qualsiasi elemento secondario di /foo. I valori di questi componenti del percorso con caratteri jolly sono disponibili nell'oggetto EventContext.params della funzione. In questo esempio, il valore è disponibile come context.params.bar.

I percorsi con caratteri jolly possono corrispondere a più eventi da una singola scrittura. Un inserimento di

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

corrisponde al percorso "/foo/{bar}" due volte: una volta con "hello": "world" e di nuovo con "firebase": "functions".

Gestire i dati sugli eventi

Quando gestisci un evento Realtime Database, l'oggetto dati restituito è un DataSnapshot. Per gli eventi onWrite o onUpdate, il primo parametro è un oggetto Change contenente due istantanee che rappresentano lo stato dei dati prima e dopo l'evento di attivazione. Per gli eventi onCreate e onDelete, l'oggetto dati restituito è uno snapshot dei dati creati o eliminati.

In questo esempio, la funzione recupera lo snapshot per il percorso specificato, converte la stringa in quella posizione in maiuscolo e scrive la stringa modificata nel 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);
    });

Accesso alle informazioni di autenticazione degli utenti

Da EventContext.auth e EventContext.authType, puoi accedere alle informazioni dell'utente che ha attivato una funzione, incluse le autorizzazioni. Questo può essere utile per applicare le regole di sicurezza, consentendo alla funzione di completare operazioni diverse in base al livello di autorizzazioni dell'utente:

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

Inoltre, puoi utilizzare le informazioni di autenticazione dell'utente per "rubare l'identità" di un utente ed eseguire operazioni di scrittura per suo conto. Assicurati di eliminare l'istanza dell'app come mostrato di seguito per evitare problemi di contemporaneità:

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

Lettura del valore precedente

L'oggetto Change ha una proprietà before che ti consente di controllare cosa è stato salvato in Realtime Database prima dell'evento. La proprietà before restituisce un DataSnapshot in cui tutti i metodi (ad esempio, val() e exists()) fa riferimento al valore precedente. Puoi leggere di nuovo il nuovo valore utilizzando DataSnapshot originale o leggendo la proprietà after. Questa proprietà su qualsiasi Change è un altro DataSnapshot che rappresenta lo stato dei dati dopo l'evento.

Ad esempio, la proprietà before può essere utilizzata per assicurarsi che la funzione metta in maiuscolo il testo solo al momento della prima creazione:

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