Étendre Cloud Firestore avec Cloud Functions (2e génération)

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

Cloud Functions (2e génération)

Basé sur Cloud Run et Eventarc, Cloud Functions for Firebase (2e génération) vous offre une infrastructure plus puissante, un contrôle avancé des performances et de l'évolutivité, et un contrôle accru sur l'environnement d'exécution des fonctions. Pour en savoir plus sur la 2e génération, consultez la page Cloud Functions pour Firebase (2e génération). Pour en savoir plus sur la 1re génération, consultez Étendre Cloud Firestore avec Cloud Functions.

Déclencheurs de fonction Cloud Firestore

Le SDK Cloud Functions for Firebase exporte les déclencheurs d'événements Cloud Firestore suivants pour vous permettre de créer des gestionnaires associés à des événements Cloud Firestore spécifiques:

Node.js

Type d'événement Déclencheur
onDocumentCreated Déclenché lorsqu'un document est écrit pour la première fois.
onDocumentUpdated Déclenché lorsqu'un document existe déjà et qu'une valeur y a été modifiée.
onDocumentDeleted Déclenché lorsqu'un document est supprimé.
onDocumentWritten Déclenché lorsque onDocumentCreated, onDocumentUpdated ou onDocumentDeleted est déclenché.
onDocumentCreatedWithAuthContext onDocumentCreated avec des informations d'authentification supplémentaires
onDocumentWrittenWithAuthContext onDocumentWritten avec des informations d'authentification supplémentaires
onDocumentDeletedWithAuthContext onDocumentDeleted avec des informations d'authentification supplémentaires
onDocumentUpdatedWithAuthContext onDocumentUpdated avec des informations d'authentification supplémentaires

Python (bêta)

Type d'événement Déclencheur
on_document_created Déclenché lorsqu'un document est écrit pour la première fois.
on_document_updated Déclenché lorsqu'un document existe déjà et qu'une valeur y a été modifiée.
on_document_deleted Déclenché lorsqu'un document est supprimé.
on_document_written Déclenché lorsque on_document_created, on_document_updated ou on_document_deleted est déclenché.
on_document_created_with_auth_context on_document_created avec des informations d'authentification supplémentaires
on_document_updated_with_auth_context on_document_updated avec des informations d'authentification supplémentaires
on_document_deleted_with_auth_context on_document_deleted avec des informations d'authentification supplémentaires
on_document_written_with_auth_context on_document_written avec des informations d'authentification supplémentaires

Les événements Cloud Firestore ne se déclenchent que lorsque le document est modifié. Une mise à jour d'un document Cloud Firestore où les données restent inchangées (écriture no-op) ne génère pas d'événement de mise à jour ou d'écriture. Il est impossible d'ajouter des événements à des champs spécifiques.

Si vous n'avez pas encore activé de projet dans Cloud Functions for Firebase, consultez la page Premiers pas avec Cloud Functions for Firebase (2e génération) pour en configurer un.Cloud Functions for Firebase

Écrire des fonctions déclenchées par Cloud Firestore

Définir un déclencheur de fonction

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

Node.js

import {
  onDocumentWritten,
  onDocumentCreated,
  onDocumentUpdated,
  onDocumentDeleted,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("my-collection/{docId}", (event) => {
   /* ... */ 
});

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_created,
  on_document_deleted,
  on_document_updated,
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_created(document="users/{userId}")
def myfunction(event: Event[DocumentSnapshot]) -> None:

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

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("users/marie", (event) => {
  // Your code here
});

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/marie")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:

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

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("users/{userId}", (event) => {
  // If we set `/users/marie` to {name: "Marie"} then
  // event.params.userId == "marie"
  // ... and ...
  // event.data.after.data() == {name: "Marie"}
});

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # If we set `/users/marie` to {name: "Marie"} then
  event.params["userId"] == "marie"  # True
  # ... and ...
  event.data.after.to_dict() == {"name": "Marie"}  # True

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 event.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

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("users/{userId}/{messageCollectionId}/{messageId}", (event) => {
    // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
    // event.params.userId == "marie";
    // event.params.messageCollectionId == "incoming_messages";
    // event.params.messageId == "134";
    // ... and ...
    // event.data.after.data() == {body: "Hello"}
});

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/{userId}/{messageCollectionId}/{messageId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
  event.params["userId"] == "marie"  # True
  event.params["messageCollectionId"] == "incoming_messages"  # True
  event.params["messageId"] == "134"  # True
  # ... and ...
  event.data.after.to_dict() == {"body": "Hello"}

Votre déclencheur doit toujours pointer vers un document, même si vous utilisez un caractère générique. Par exemple, users/{userId}/{messageCollectionId} n'est pas valide, car {messageCollectionId} est une collection. En revanche, users/{userId}/{messageCollectionId}/{messageId} est valide, car {messageId} pointe toujours vers un document.

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. Cet exemple de fonction se déclenche chaque fois qu'un nouveau profil utilisateur est ajouté:

Node.js

import {
  onDocumentCreated,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.createuser = onDocumentCreated("users/{userId}", (event) => {
    // Get an object representing the document
    // e.g. {'name': 'Marie', 'age': 66}
    const snapshot = event.data;
    if (!snapshot) {
        console.log("No data associated with the event");
        return;
    }
    const data = snapshot.data();

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

    // perform more operations ...
});

Pour obtenir des informations d'authentification supplémentaires, utilisez onDocumentCreatedWithAuthContext.

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_created,
  Event,
  DocumentSnapshot,
)

@on_document_created(document="users/{userId}")
def myfunction(event: Event[DocumentSnapshot]) -> None:
  # Get a dictionary representing the document
  # e.g. {'name': 'Marie', 'age': 66}
  new_value = event.data.to_dict()

  # Access a particular field as you would any dictionary
  name = new_value["name"]

  # Perform more operations ...

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

Vous pouvez également déclencher une fonction lorsqu'un document est mis à jour. Cet exemple de fonction se déclenche si un utilisateur modifie son profil:

Node.js

import {
  onDocumentUpdated,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.updateuser = onDocumentUpdated("users/{userId}", (event) => {
    // Get an object representing the document
    // e.g. {'name': 'Marie', 'age': 66}
    const newValue = event.data.after.data();

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

    // perform more operations ...
});

Pour obtenir des informations d'authentification supplémentaires, utilisez onDocumentUpdatedWithAuthContext.

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_updated,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_updated(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # Get a dictionary representing the document
  # e.g. {'name': 'Marie', 'age': 66}
  new_value = event.data.after.to_dict()

  # Access a particular field as you would any dictionary
  name = new_value["name"]

  # Perform more operations ...

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

Vous pouvez également déclencher une fonction lorsqu'un document est supprimé. Cet exemple de fonction se déclenche lorsqu'un utilisateur supprime son profil utilisateur:

Node.js

import {
  onDocumentDeleted,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.deleteuser = onDocumentDeleted("users/{userId}", (event) => {
    // Get an object representing the document
    // e.g. {'name': 'Marie', 'age': 66}
    const snap =  event.data;
    const data =  snap.data();

    // perform more operations ...
});

Pour obtenir des informations d'authentification supplémentaires, utilisez onDocumentDeletedWithAuthContext.

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_deleted,
  Event,
  DocumentSnapshot,
)

@on_document_deleted(document="users/{userId}")
def myfunction(event: Event[DocumentSnapshot|None]) -> None:
  # Perform more 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 Cloud Firestore à l'aide du déclencheur d'événement "document écrit". Cet exemple de fonction se déclenche si un utilisateur est créé, mis à jour ou supprimé:

Node.js

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.modifyuser = onDocumentWritten("users/{userId}", (event) => {
    // Get an object with the current document values.
    // If the document does not exist, it was deleted
    const document =  event.data.after.data();

    // Get an object with the previous document values
    const previousValues =  event.data.before.data();

    // perform more operations ...
});

Pour obtenir des informations d'authentification supplémentaires, utilisez onDocumentWrittenWithAuthContext.

Python (bêta)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot | None]]) -> None:
  # Get an object with the current document values.
  # If the document does not exist, it was deleted.
  document = (event.data.after.to_dict()
              if event.data.after is not None else None)

  # Get an object with the previous document values.
  # If the document does not exist, it was newly created.
  previous_values = (event.data.before.to_dict()
                     if event.data.before is not None else None)

  # Perform more 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 event.data.before, qui contient l'instantané du document avant sa mise à jour. De même, event.data.after contient l'état de l'instantané du document après sa mise à jour.

Node.js

exports.updateuser2 = onDocumentUpdated("users/{userId}", (event) => {
    // Get an object with the current document values.
    // If the document does not exist, it was deleted
    const newValues =  event.data.after.data();

    // Get an object with the previous document values
    const previousValues =  event.data.before.data();
});

Python (bêta)

@on_document_updated(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # Get an object with the current document values.
  new_value = event.data.after.to_dict()

  # Get an object with the previous document values.
  prev_value = event.data.before.to_dict()

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 = event.data.after.data().age;
const name = event.data.after.data()['name'];

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

Python (bêta)

# Get the value of a single document field.
age = event.data.after.get("age")

# Convert the document to a dictionary.
age = event.data.after.to_dict()["age"]

Écrire des données

Chaque appel de fonction est associé à un document spécifique de votre base de données Cloud Firestore. Vous pouvez accéder à ce document dans l'instantané renvoyé à votre fonction.

La référence de document inclut des méthodes telles que update(), set() et remove(), qui vous permettent de modifier le document ayant déclenché la fonction.

Node.js

import { onDocumentUpdated } from "firebase-functions/v2/firestore";

exports.countnamechanges = onDocumentUpdated('users/{userId}', (event) => {
  // Retrieve the current and previous value
  const data = event.data.after.data();
  const previousData = event.data.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 data.after.ref.set({
    name_change_count: count + 1
  }, {merge: true});

});

Python (bêta)

@on_document_updated(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # Get the current and previous document values.
  new_value = event.data.after
  prev_value = event.data.before

  # We'll only update if the name has changed.
  # This is crucial to prevent infinite loops.
  if new_value.get("name") == prev_value.get("name"):
      return

  # Retrieve the current count of name changes
  count = new_value.to_dict().get("name_change_count", 0)

  # Update the count
  new_value.reference.update({"name_change_count": count + 1})

Accéder aux informations d'authentification des utilisateurs

Si vous utilisez l'un des types d'événements suivants, vous pouvez accéder aux informations d'authentification utilisateur sur le principal qui a déclenché l'événement. Ces informations s'ajoutent à celles renvoyées dans l'événement de base.

Node.js

  • onDocumentCreatedWithAuthContext
  • onDocumentWrittenWithAuthContext
  • onDocumentDeletedWithAuthContext
  • onDocumentUpdatedWithAuthContext

Python (bêta)

  • on_document_created_with_auth_context
  • on_document_updated_with_auth_context
  • on_document_deleted_with_auth_context
  • on_document_written_with_auth_context

Pour en savoir plus sur les données disponibles dans le contexte d'authentification, consultez la section Contexte d'authentification. L'exemple suivant montre comment récupérer des informations d'authentification:

Node.js

import { onDocumentWrittenWithAuthContext } from "firebase-functions/v2/firestore"

exports.syncUser = onDocumentWrittenWithAuthContext("users/{userId}", (event) => {
    const snapshot = event.data.after;
    if (!snapshot) {
        console.log("No data associated with the event");
        return;
    }
    const data = snapshot.data();

    // retrieve auth context from event
    const { authType, authId } = event;

    let verified = false;
    if (authType === "system") {
      // system-generated users are automatically verified
      verified = true;
    } else if (authType === "unknown" || authType === "unauthenticated") {
      // admin users from a specific domain are verified
      if (authId.endsWith("@example.com")) {
        verified = true;
      }
    }

    return data.after.ref.set({
        created_by: authId,
        verified,
    }, {merge: true}); 
}); 

Python (bêta)

@on_document_updated_with_auth_context(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:

  # Get the current and previous document values.
  new_value = event.data.after
  prev_value = event.data.before

  # Get the auth context from the event
  user_auth_type = event.auth_type
  user_auth_id = event.auth_id

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

Cloud Functions s'exécute dans un environnement sécurisé. Il est autorisé en tant que compte de service dans votre projet, et vous pouvez effectuer des lectures et des écritures à l'aide du SDK Firebase Admin:

Node.js

const { initializeApp } = require('firebase-admin/app');
const { getFirestore, Timestamp, FieldValue } = require('firebase-admin/firestore');

initializeApp();
const db = getFirestore();

exports.writetofirestore = onDocumentWritten("some/doc", (event) => {
    db.doc('some/otherdoc').set({ ... });
  });

  exports.writetofirestore = onDocumentWritten('users/{userId}', (event) => {
    db.doc('some/otherdoc').set({
      // Update otherdoc
    });
  });

Python (bêta)

from firebase_admin import firestore, initialize_app
import google.cloud.firestore

initialize_app()

@on_document_written(document="some/doc")
def myfunction(event: Event[Change[DocumentSnapshot | None]]) -> None:
  firestore_client: google.cloud.firestore.Client = firestore.client()
  firestore_client.document("another/doc").set({
      # ...
  })

Limites

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

  • Cloud 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 Cloud Firestore ni avec le mode Datastore. Veuillez utiliser Cloud Functions (2e génération) 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.
  • Cloud Firestore en mode Datastore nécessite Cloud Functions (2e génération). Cloud 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 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 Functions (2e génération)
      • Réduire la taille du document
      • Supprimer l'Cloud 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.