Étendre Firestore avec Cloud Functions

Avec Cloud Functions, vous pouvez déployer du code Node.js pour gérer les événements déclenchés lorsque votre base de données Firestore est modifiée. Ce service vous permet d'ajouter facilement des fonctionnalités côté serveur à votre application, sans avoir à gérer vos propres serveurs.

Pour obtenir des exemples de cas d'utilisation, consultez la page Que puis-je faire avec Cloud Functions ? ou le dépôt Extraits de fonctions sur GitHub.

Déclencheurs de fonction Firestore

Le SDK Cloud Functions for Firebase exporte un objet functions.firestore qui vous permet de créer des gestionnaires associés à des événements Firestore spécifiques.

Type d'événement Déclencheur
onCreate Déclenché lorsqu'un document est écrit pour la première fois.
onUpdate Déclenché lorsqu'un document existe déjà et qu'une valeur y a été modifiée.
onDelete Déclenché lorsqu'un document contenant des données est supprimé.
onWrite Déclenché lorsque onCreate, onUpdate ou onDelete est déclenché.

Si vous n'avez pas encore activé de projet dans Cloud Functions for Firebase, consultez la page Premiers pas : écrire et déployer vos premières fonctions pour en configurer un.

Écrire des fonctions déclenchées par Firestore

Définir un déclencheur de fonction

Pour définir un déclencheur Firestore, spécifiez un chemin de document et un type d'événement :

Node.js

const functions = require('firebase-functions');

exports.myFunction = functions.firestore
  .document('my-collection/{docId}')
  .onWrite((change, context) => { /* ... */ });

Les chemins de document peuvent faire référence soit à un document spécifique, soit à un format contenant des caractères génériques.

Spécifier un document unique

Si vous souhaitez déclencher un événement chaque fois qu'une modification est constatée sur un document spécifique, vous pouvez utiliser la fonction suivante.

Node.js

// Listen for any change on document `marie` in collection `users`
exports.myFunctionName = functions.firestore
    .document('users/marie').onWrite((change, context) => {
      // ... Your code here
    });

Spécifier un groupe de documents à l'aide de caractères génériques

Si vous souhaitez associer un déclencheur à un groupe de documents, tel que l'ensemble des documents d'une collection spécifique, utilisez un caractère générique {wildcard} à la place de l'identifiant du document :

Node.js

// Listen for changes in all documents in the 'users' collection
exports.useWildcard = functions.firestore
    .document('users/{userId}')
    .onWrite((change, context) => {
      // If we set `/users/marie` to {name: "Marie"} then
      // context.params.userId == "marie"
      // ... and ...
      // change.after.data() == {name: "Marie"}
    });

Dans cet exemple, lorsqu'un champ de n'importe quel document de la collection users est modifié, il correspond au caractère générique userId.

Si un document de la collection users comporte des sous-collections et qu'un champ de l'une d'entre elles est modifié, le caractère générique userId n'est pas déclenché.

Les correspondances de caractères génériques sont extraites du chemin du document et stockées dans le fichier context.params. Vous pouvez définir autant de caractères génériques que vous le souhaitez pour remplacer les identifiants explicites de collection ou de document, par exemple :

Node.js

// Listen for changes in all documents in the 'users' collection and all subcollections
exports.useMultipleWildcards = functions.firestore
    .document('users/{userId}/{messageCollectionId}/{messageId}')
    .onWrite((change, context) => {
      // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
      // context.params.userId == "marie";
      // context.params.messageCollectionId == "incoming_messages";
      // context.params.messageId == "134";
      // ... and ...
      // change.after.data() == {body: "Hello"}
    });

Déclencheurs d'événements

Déclencher une fonction lorsqu'un document est créé

Vous pouvez déclencher une fonction chaque fois qu'un document est créé dans une collection à l'aide d'un gestionnaire onCreate() associé à un caractère générique. Cet exemple de fonction appelle createUser chaque fois qu'un nouveau profil utilisateur est ajouté :

Node.js

exports.createUser = functions.firestore
    .document('users/{userId}')
    .onCreate((snap, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = snap.data();

      // access a particular field as you would any JS property
      const name = newValue.name;

      // perform desired operations ...
    });

Déclencher une fonction lorsqu'un document est mis à jour

Vous pouvez également déclencher une fonction lorsqu'un document est mis à jour à l'aide de la fonction onUpdate() associée à un caractère générique. Cet exemple de fonction appelle updateUser si un utilisateur modifie son profil :

Node.js

exports.updateUser = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();

      // access a particular field as you would any JS property
      const name = newValue.name;

      // perform desired operations ...
    });

Déclencher une fonction lorsqu'un document est supprimé

Vous pouvez également déclencher une fonction lorsqu'un document est supprimé à l'aide de la fonction onDelete() associée à un caractère générique. Cet exemple de fonction appelle deleteUser lorsqu'un utilisateur supprime son profil utilisateur :

Node.js

exports.deleteUser = functions.firestore
    .document('users/{userID}')
    .onDelete((snap, context) => {
      // Get an object representing the document prior to deletion
      // e.g. {'name': 'Marie', 'age': 66}
      const deletedValue = snap.data();

      // perform desired operations ...
    });

Déclencher une fonction pour toute modification apportée à un document

Si le type d'événement déclenché n'a pas d'importance pour vous, vous pouvez écouter toutes les modifications d'un document Firestore à l'aide de la fonction onWrite() associée à un caractère générique. Cet exemple de fonction appelle modifyUser si un utilisateur est créé, mis à jour ou supprimé :

Node.js

exports.modifyUser = functions.firestore
    .document('users/{userID}')
    .onWrite((change, context) => {
      // Get an object with the current document value.
      // If the document does not exist, it has been deleted.
      const document = change.after.exists ? change.after.data() : null;

      // Get an object with the previous document value (for update or delete)
      const oldDocument = change.before.data();

      // perform desired operations ...
    });

Lire et écrire des données

Lorsqu'une fonction est déclenchée, elle fournit un instantané des données associées à l'événement. Vous pouvez utiliser cet instantané pour lire ou écrire dans le document ayant déclenché l'événement, ou utiliser le SDK Firebase Admin pour accéder à d'autres parties de votre base de données.

Données d'événement

Lire les données

Lorsqu'une fonction est déclenchée, vous pouvez obtenir les données d'un document mis à jour ou récupérer la version antérieure à la mise à jour. Vous pouvez obtenir la version antérieure en utilisant change.before.data(), qui contient l'instantané du document avant sa mise à jour. De même, change.after.data() contient l'état de l'instantané du document après sa mise à jour.

Node.js

exports.updateUser2 = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the current document
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();
    });

Vous pouvez accéder aux propriétés comme vous le feriez pour n'importe quel autre objet. Vous pouvez également utiliser la fonction get pour accéder à des champs spécifiques :

Node.js

// Fetch data using standard accessors
const age = snap.data().age;
const name = snap.data()['name'];

// Fetch data using built in accessor
const experience = snap.get('experience');

Écrire des données

Chaque appel de fonction est associé à un document spécifique de votre base de données Firestore. La propriété ref de l'instantané renvoyé à votre fonction vous permet d'accéder à ce document en tant que DocumentReference.

DocumentReference provient du SDK Node.js Firestore et inclut des méthodes telles que update(), set() et remove(), qui vous permettent de modifier facilement le document ayant déclenché la fonction.

Node.js

// Listen for updates to any `user` document.
exports.countNameChanges = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Retrieve the current and previous value
      const data = change.after.data();
      const previousData = change.before.data();

      // We'll only update if the name has changed.
      // This is crucial to prevent infinite loops.
      if (data.name == previousData.name) {
        return null;
      }

      // Retrieve the current count of name changes
      let count = data.name_change_count;
      if (!count) {
        count = 0;
      }

      // Then return a promise of a set operation to update the count
      return change.after.ref.set({
        name_change_count: count + 1
      }, {merge: true});
    });

Données situées à l'extérieur de l'événement déclencheur

Cloud Functions permet d'exécuter des fonctions dans un environnement de confiance, ce qui signifie qu'elles sont autorisées en tant que compte de service dans votre projet. Vous pouvez accéder aux données en lecture et en écriture à l'aide du SDK Firebase Admin :

Node.js

const admin = require('firebase-admin');
admin.initializeApp();

const db = admin.firestore();

exports.writeToFirestore = functions.firestore
  .document('some/doc')
  .onWrite((change, context) => {
    db.doc('some/otherdoc').set({ ... });
  });

Limites

Notez les limites suivantes concernant les déclencheurs Firestore pour Cloud Run Functions :

  • Cloud Run Functions (1re génération) nécessite une base de données "(default)" existante en mode natif Firestore. La solution n'est pas compatible avec les bases de données nommées Firestore ni avec le mode Datastore. Veuillez utiliser Cloud Run Functions (2nd gen) pour configurer des événements dans ce cas.
  • L'ordre n'est pas garanti. Les modifications rapides peuvent déclencher des appels de fonctions dans un ordre inattendu.
  • Bien que les événements soient diffusés une fois au moins, un même événement peut produire plusieurs appels de fonction. Évitez de dépendre de procédés dits "exactement une fois" et écrivez des fonctions idempotentes.
  • Firestore en mode Datastore nécessite Cloud Run Functions (2nd gen). Cloud Run Functions (1re génération) n'est pas compatible avec le mode Datastore.
  • Un déclencheur est associé à une seule base de données. Vous ne pouvez pas créer un déclencheur qui correspond à plusieurs bases de données.
  • La suppression d'une base de données ne supprime pas automatiquement les déclencheurs de cette base de données. Le déclencheur cesse de diffuser des événements, mais continue d'exister jusqu'à ce que vous le supprimiez.
  • Si un événement correspondant dépasse la taille maximale de requête, il risque de ne pas être distribué à Cloud Run Functions (1re génération).
    • Les événements non distribués en raison de la taille de la requête sont consignés dans les journaux de plate-forme et sont comptabilisés dans l'utilisation des journaux du projet.
    • Vous trouverez ces journaux dans l'explorateur de journaux avec le message "Event cannot deliver to Cloud function due to size exceeding the limit for 1st gen..." (l'événement ne peut pas être distribué à la fonction Cloud, car sa taille dépasse la limite pour la 1re génération...) de gravité error. Vous trouverez le nom de la fonction dans le champ functionName. Si le champ receiveTimestamp date de moins d'une heure, vous pouvez déduire le contenu réel de l'événement en lisant le document en question avec un instantané avant et après le code temporel.
    • Pour éviter une telle cadence, vous pouvez :
      • Migrer et passer à Cloud Run Functions (2nd gen)
      • Réduire la taille du document
      • Supprimer les fonctions Cloud Run Functions en question
    • Vous pouvez désactiver la journalisation proprement dite à l'aide d'exclusions, mais notez que les événements mis en cause ne seront toujours pas distribués.