Étendre Realtime Database avec Cloud Functions


Avec Cloud Functions, vous pouvez gérer les événements dans Firebase Realtime Database sans avoir à mettre à jour le code client. Cloud Functions vous permet d'exécuter des opérations Realtime Database avec des droits d'administrateur complets et garantit que chaque modification apportée à Realtime Database est traitée individuellement. Vous pouvez apporter des modifications à Firebase Realtime Database via DataSnapshot ou le SDK Admin.

Dans un cycle de vie typique, une fonction Firebase Realtime Database effectue les opérations suivantes:

  1. Attend les modifications apportées à un emplacement Realtime Database spécifique.
  2. Se déclenche lorsqu'un événement se produit et effectue ses tâches (consultez la section Que puis-je faire avec Cloud Functions ?) pour obtenir des exemples de cas d'utilisation).
  3. Reçoit un objet de données qui contient un instantané des données stockées dans le document spécifié.

Déclencher une fonction Realtime Database

Créez des fonctions pour les événements Realtime Database avec functions.database. Pour contrôler le moment où la fonction se déclenche, spécifiez l'un des gestionnaires d'événements et le chemin d'accès Realtime Database où il écoutera les événements.

Définir le gestionnaire d'événements

Cloud Functions vous permet de gérer les événements Realtime Database à deux niveaux de spécificité. Vous pouvez écouter uniquement les événements de création, de mise à jour ou de suppression ou bien écouter toute modification apportée à un chemin d'accès. Cloud Functions est compatible avec les gestionnaires d'événements suivants pour Realtime Database :

  • onWrite(), qui se déclenche lorsque des données sont créées, mises à jour ou supprimées dans Realtime Database.
  • onCreate(), qui se déclenche lorsque de nouvelles données sont créées dans Realtime Database.
  • onUpdate(), qui se déclenche lorsque les données sont mises à jour dans Realtime Database .
  • onDelete(), qui se déclenche lorsque des données sont supprimées de Realtime Database .

Spécifier l'instance et le chemin d'accès

Pour contrôler le moment et le lieu du déclenchement de votre fonction, appelez ref(path) pour spécifier un chemin d'accès et éventuellement une instance Realtime Database avec instance('INSTANCE_NAME'). Si vous ne spécifiez pas d'instance, la fonction est déployée sur l'instance Realtime Database par défaut du projet Firebase. Par exemple:

  • Instance Realtime Database par défaut : functions.database.ref('/foo/bar')
  • Instance nommée "my-app-db-2" : functions.database.instance('my-app-db-2').ref('/foo/bar')

Ces méthodes indiquent à votre fonction de gérer les écritures à un certain chemin d'accès dans l'instance Realtime Database. Les spécifications de chemin d'accès correspondent à toutes les écritures qui touchent un chemin d'accès, y compris les écritures se trouvant en dessous. Si vous définissez le chemin d'accès de votre fonction sur /foo/bar, il correspond aux événements de ces deux emplacements :

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

Dans les deux cas, Firebase interprète que l'événement se produit à /foo/bar et que les données d'événement incluent les anciennes et les nouvelles données se trouvant à /foo/bar. Si les données d'événement risquent d'être volumineuses, envisagez d'utiliser plusieurs fonctions sur des chemins d'accès plus profonds au lieu d'utiliser une seule fonction près de la racine de votre base de données. Pour des performances optimales, demandez uniquement des données au niveau le plus profond possible.

Vous pouvez spécifier un composant de chemin d'accès en tant que caractère générique en l'entourant d'accolades. ref('foo/{bar}') correspond à n'importe quel enfant de /foo. Les valeurs de ces composants de chemin d'accès à caractère générique sont disponibles dans l'objet EventContext.params de votre fonction. Dans cet exemple, la valeur est disponible sous le nom context.params.bar.

Les chemins d'accès contenant des caractères génériques peuvent correspondre à plusieurs événements d'une seule écriture. Une insertion de

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

correspond au chemin d'accès "/foo/{bar}" deux fois: une fois avec "hello": "world" et une autre fois avec "firebase": "functions".

Gérer les données d'événement

Lors de la gestion d'un événement Realtime Database, l'objet de données renvoyé est un DataSnapshot. Pour les événements onWrite ou onUpdate, le premier paramètre est un objet Change contenant deux instantanés représentant l'état des données avant et après l'événement déclencheur. Pour les événements onCreate et onDelete, l'objet de données renvoyé est un instantané des données créées ou supprimées.

Dans cet exemple, la fonction récupère l'instantané pour le chemin spécifié, convertit la chaîne à cet emplacement en majuscules et écrit cette chaîne modifiée dans la base de données:

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

Accéder aux informations d'authentification des utilisateurs

À partir de EventContext.auth et EventContext.authType, vous pouvez accéder aux informations sur l'utilisateur ayant déclenché une fonction, y compris les autorisations. Cela peut être utile pour appliquer des règles de sécurité, ce qui permet à votre fonction d'effectuer différentes opérations en fonction du niveau d'autorisations de l'utilisateur:

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

Vous pouvez également exploiter les informations d'authentification de l'utilisateur pour "usurper" l'identité d'un utilisateur et effectuer des opérations d'écriture en son nom. Veillez à supprimer l'instance d'application comme indiqué ci-dessous pour éviter les problèmes de simultanéité:

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

Lire la valeur précédente

L'objet Change possède une propriété before qui vous permet d'inspecter ce qui a été enregistré dans Realtime Database avant l'événement. La propriété before renvoie un DataSnapshot où toutes les méthodes (par exemple, val() et exists()) font référence à la valeur précédente. Vous pouvez lire à nouveau la nouvelle valeur en utilisant l'DataSnapshot d'origine ou en lisant la propriété after. Cette propriété sur n'importe quel Change est un autre DataSnapshot représentant l'état des données après que l'événement s'est produit.

Par exemple, vous pouvez utiliser la propriété before pour vous assurer que la fonction ne met du texte en majuscules que lors de sa création:

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