Déclencheurs de Google Cloud Firestore

Cloud Functions peut gérer les événements dans Cloud Firestore dans le même projet Cloud contenant la fonction. Vous pouvez lire et/ou mettre à jour Cloud Firestore en réponse à ces événements à l'aide des API Firestore et des bibliothèques clientes.

Dans un cycle de vie typique, une fonction Cloud Firestore effectue les opérations suivantes :

  1. Attend les modifications apportées à un document donné.

  2. Se déclenche lorsqu'un événement se produit et exécute ses tâches.

  3. Reçoit un objet de données contenant un instantané du document affecté. Pour les événements write ou update, l'objet de données contient des instantanés représentant l'état du document avant et après l'événement déclencheur.

Types d'événement

Cloud Firestore accepte les événements create, update, delete et write. L'événement write englobe toutes les modifications apportées à un document.

Type d'événement Déclencheur
providers/cloud.firestore/eventTypes/document.create (par défaut) Déclenché lorsqu'un document est écrit pour la première fois.
providers/cloud.firestore/eventTypes/document.update Déclenché lorsqu'un document existe déjà et qu'une valeur y a été modifiée.
providers/cloud.firestore/eventTypes/document.delete Déclenché lorsqu'un document contenant des données est supprimé.
providers/cloud.firestore/eventTypes/document.write Déclenché lorsqu'un document est créé, mis à jour ou supprimé.

Les caractères génériques sont écrits dans les déclencheurs à l'aide d'accolades, comme suit : /projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

Spécifier le chemin d'accès du document

Pour déclencher votre fonction, spécifiez un chemin d'accès de document à écouter. Les fonctions ne réagissent qu'aux modifications de document et ne peuvent pas surveiller des champs ou des collections spécifiques. Voici quelques exemples de chemins d'accès de document valides :

  • users/marie : déclencheur valide. Surveille un seul document, /users/marie.

  • users/{username} : déclencheur valide. Surveille tous les documents d'utilisateur. Les caractères génériques permettent de surveiller tous les documents de la collection.

  • users/{username}/addresses : déclencheur non valide. Renvoie à la sous-collection addresses, et non à un document.

  • users/{username}/addresses/home : déclencheur valide. Surveille le document d'adresses personnelles de tous les utilisateurs.

  • users/{username}/addresses/{addressId} : déclencheur valide. Surveille tous les documents d'adresses.

Utiliser des caractères génériques et des paramètres

Si vous ne connaissez pas le nom du document que vous souhaitez surveiller, utilisez un caractère générique ({wildcard}) à la place de l'ID du document :

  • users/{username} : écoute les modifications apportées à tous les documents d'utilisateur.

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 {username}.

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 {username} n'est pas déclenché.

Les correspondances de caractères génériques sont extraites du chemin du document. Vous pouvez définir autant de caractères génériques que vous le souhaitez pour remplacer les ID explicites de collection ou de document.

Structure de l'événement

Ce déclencheur appelle votre fonction avec un événement semblable à celui présenté ci-dessous :

{
    "oldValue": { // Update and Delete operations only
        A Document object containing a pre-operation document snapshot
    },
    "updateMask": { // Update operations only
        A DocumentMask object that lists changed fields.
    },
    "value": {
        // A Document object containing a post-operation document snapshot
    }
}

Chaque objet Document contient un ou plusieurs objets Value. Consultez la documentation de référence Value pour obtenir plus d'informations sur les types de valeurs. C'est particulièrement utile si vous utilisez un langage saisi, tel que Go, pour écrire vos fonctions.

Exemple de code

L'exemple de fonction Cloud ci-dessous imprime les champs d'un événement déclencheur Cloud Firestore :

Node.js

/**
 * Triggered by a change to a Firestore document.
 *
 * @param {!Object} event The Cloud Functions event.
 */
exports.helloFirestore = event => {
  const triggerResource = event.resource;

  console.log(`Function triggered by event on: ${triggerResource}`);
  console.log(`Event type: ${event.eventType}`);

  if (event.oldValue && Object.keys(event.oldValue).length) {
    console.log('\nOld value:');
    console.log(JSON.stringify(event.oldValue, null, 2));
  }

  if (event.value && Object.keys(event.value).length) {
    console.log('\nNew value:');
    console.log(JSON.stringify(event.value, null, 2));
  }
};

Python

import json
def hello_firestore(data, context):
    """ Triggered by a change to a Firestore document.
    Args:
        data (dict): The event payload.
        context (google.cloud.functions.Context): Metadata for the event.
    """
    trigger_resource = context.resource

    print('Function triggered by change to: %s' % trigger_resource)

    print('\nOld value:')
    print(json.dumps(data["oldValue"]))

    print('\nNew value:')
    print(json.dumps(data["value"]))

Go


// Package hello contains a Cloud Function triggered by a Firestore event.
package hello

import (
	"context"
	"fmt"
	"log"
	"time"

	"cloud.google.com/go/functions/metadata"
)

// FirestoreEvent is the payload of a Firestore event.
type FirestoreEvent struct {
	OldValue   FirestoreValue `json:"oldValue"`
	Value      FirestoreValue `json:"value"`
	UpdateMask struct {
		FieldPaths []string `json:"fieldPaths"`
	} `json:"updateMask"`
}

// FirestoreValue holds Firestore fields.
type FirestoreValue struct {
	CreateTime time.Time `json:"createTime"`
	// Fields is the data for this value. The type depends on the format of your
	// database. Log the interface{} value and inspect the result to see a JSON
	// representation of your database fields.
	Fields     interface{} `json:"fields"`
	Name       string      `json:"name"`
	UpdateTime time.Time   `json:"updateTime"`
}

// HelloFirestore is triggered by a change to a Firestore document.
func HelloFirestore(ctx context.Context, e FirestoreEvent) error {
	meta, err := metadata.FromContext(ctx)
	if err != nil {
		return fmt.Errorf("metadata.FromContext: %v", err)
	}
	log.Printf("Function triggered by change to: %v", meta.Resource)
	log.Printf("Old value: %+v", e.OldValue)
	log.Printf("New value: %+v", e.Value)
	return nil
}

Java

import com.google.cloud.functions.Context;
import com.google.cloud.functions.RawBackgroundFunction;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.util.logging.Logger;

public class FirebaseFirestore implements RawBackgroundFunction {
  private static final Logger logger = Logger.getLogger(FirebaseFirestore.class.getName());

  // Use GSON (https://github.com/google/gson) to parse JSON content.
  private static final Gson gson = new Gson();

  @Override
  public void accept(String json, Context context) {
    JsonObject body = gson.fromJson(json, JsonObject.class);
    logger.info("Function triggered by event on: " + context.resource());
    logger.info("Event type: " + context.eventType());

    if (body != null && body.has("oldValue")) {
      logger.info("Old value:");
      logger.info(body.get("oldValue").getAsString());
    }

    if (body != null && body.has("value")) {
      logger.info("New value:");
      logger.info(body.get("value").getAsString());
    }
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace FirebaseFirestore
{
    public class Function : ICloudEventFunction<DocumentEventData>
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
        {
            _logger.LogInformation("Function triggered by event on {subject}", cloudEvent.Subject);
            _logger.LogInformation("Event type: {type}", cloudEvent.Type);
            MaybeLogDocument("Old value", data.OldValue);
            MaybeLogDocument("New value", data.Value);

            // In this example, we don't need to perform any asynchronous operations, so the
            // method doesn't need to be declared async.
            return Task.CompletedTask;
        }

        /// <summary>
        /// Logs the names and values of the fields in a document in a very simplistic way.
        /// </summary>
        private void MaybeLogDocument(string message, Document document)
        {
            if (document is null)
            {
                return;
            }

            // ConvertFields converts the Firestore representation into a .NET-friendly
            // representation.
            IReadOnlyDictionary<string, object> fields = document.ConvertFields();
            var fieldNamesAndTypes = fields
                .OrderBy(pair => pair.Key)
                .Select(pair => $"{pair.Key}: {pair.Value}");
            _logger.LogInformation(message + ": {fields}", string.Join(", ", fieldNamesAndTypes));
        }
    }
}

Ruby

require "functions_framework"

# Triggered by a change to a Firestore document.
FunctionsFramework.cloud_event "hello_firestore" do |event|
  # The event parameter is a CloudEvents::Event::V1 object.
  # See https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event/V1.html
  payload = event.data

  logger.info "Function triggered by change to: #{event.source}"
  logger.info "Old value: #{payload['oldValue']}"
  logger.info "New value: #{payload['value']}"
end

L'exemple ci-dessous récupère la valeur ajoutée par l'utilisateur, convertit la chaîne à cet emplacement en majuscules, et remplace la valeur par la chaîne en majuscules :

Node.js

const Firestore = require('@google-cloud/firestore');

const firestore = new Firestore({
  projectId: process.env.GOOGLE_CLOUD_PROJECT,
});

// Converts strings added to /messages/{pushId}/original to uppercase
exports.makeUpperCase = event => {
  const resource = event.value.name;
  const affectedDoc = firestore.doc(resource.split('/documents/')[1]);

  const curValue = event.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();

  if (curValue !== newValue) {
    console.log(`Replacing value: ${curValue} --> ${newValue}`);

    return affectedDoc.set({
      original: newValue,
    });
  } else {
    // Value is already upper-case
    // Don't perform a(nother) write to avoid infinite loops
    console.log('Value is already upper-case.');
  }
};

Python

from google.cloud import firestore
client = firestore.Client()

# Converts strings added to /messages/{pushId}/original to uppercase
def make_upper_case(data, context):
    path_parts = context.resource.split('/documents/')[1].split('/')
    collection_path = path_parts[0]
    document_path = '/'.join(path_parts[1:])

    affected_doc = client.collection(collection_path).document(document_path)

    cur_value = data["value"]["fields"]["original"]["stringValue"]
    new_value = cur_value.upper()

    if cur_value != new_value:
        print(f'Replacing value: {cur_value} --> {new_value}')
        affected_doc.set({
            u'original': new_value
        })
    else:
        # Value is already upper-case
        # Don't perform a second write (which can trigger an infinite loop)
        print('Value is already upper-case.')

Go


// Package upper contains a Firestore Cloud Function.
package upper

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go/v4"
)

// FirestoreEvent is the payload of a Firestore event.
type FirestoreEvent struct {
	OldValue   FirestoreValue `json:"oldValue"`
	Value      FirestoreValue `json:"value"`
	UpdateMask struct {
		FieldPaths []string `json:"fieldPaths"`
	} `json:"updateMask"`
}

// FirestoreValue holds Firestore fields.
type FirestoreValue struct {
	CreateTime time.Time `json:"createTime"`
	// Fields is the data for this value. The type depends on the format of your
	// database. Log an interface{} value and inspect the result to see a JSON
	// representation of your database fields.
	Fields     MyData    `json:"fields"`
	Name       string    `json:"name"`
	UpdateTime time.Time `json:"updateTime"`
}

// MyData represents a value from Firestore. The type definition depends on the
// format of your database.
type MyData struct {
	Original struct {
		StringValue string `json:"stringValue"`
	} `json:"original"`
}

// GOOGLE_CLOUD_PROJECT is automatically set by the Cloud Functions runtime.
var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")

// client is a Firestore client, reused between function invocations.
var client *firestore.Client

func init() {
	// Use the application default credentials.
	conf := &firebase.Config{ProjectID: projectID}

	// Use context.Background() because the app/client should persist across
	// invocations.
	ctx := context.Background()

	app, err := firebase.NewApp(ctx, conf)
	if err != nil {
		log.Fatalf("firebase.NewApp: %v", err)
	}

	client, err = app.Firestore(ctx)
	if err != nil {
		log.Fatalf("app.Firestore: %v", err)
	}
}

// MakeUpperCase is triggered by a change to a Firestore document. It updates
// the `original` value of the document to upper case.
func MakeUpperCase(ctx context.Context, e FirestoreEvent) error {
	fullPath := strings.Split(e.Value.Name, "/documents/")[1]
	pathParts := strings.Split(fullPath, "/")
	collection := pathParts[0]
	doc := strings.Join(pathParts[1:], "/")

	curValue := e.Value.Fields.Original.StringValue
	newValue := strings.ToUpper(curValue)
	if curValue == newValue {
		log.Printf("%q is already upper case: skipping", curValue)
		return nil
	}
	log.Printf("Replacing value: %q -> %q", curValue, newValue)

	data := map[string]string{"original": newValue}
	_, err := client.Collection(collection).Doc(doc).Set(ctx, data)
	if err != nil {
		return fmt.Errorf("Set: %v", err)
	}
	return nil
}

Java


import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.SetOptions;
import com.google.cloud.functions.Context;
import com.google.cloud.functions.RawBackgroundFunction;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class FirebaseFirestoreReactive implements RawBackgroundFunction {

  // Use GSON (https://github.com/google/gson) to parse JSON content.
  private static final Gson gson = new Gson();

  private static final Logger logger = Logger.getLogger(FirebaseFirestoreReactive.class.getName());
  private static final Firestore FIRESTORE = FirestoreOptions.getDefaultInstance().getService();

  private final Firestore firestore;

  public FirebaseFirestoreReactive() {
    this(FIRESTORE);
  }

  FirebaseFirestoreReactive(Firestore firestore) {
    this.firestore = firestore;
  }

  @Override
  public void accept(String json, Context context) {
    // Get the recently-written value
    JsonObject body = gson.fromJson(json, JsonObject.class);
    JsonObject tempJson = body.getAsJsonObject("value");

    // Verify that value.fields.original.stringValue exists
    String currentValue = null;
    if (tempJson != null) {
      tempJson = tempJson.getAsJsonObject("fields");
    }
    if (tempJson != null) {
      tempJson = tempJson.getAsJsonObject("original");
    }
    if (tempJson != null && tempJson.has("stringValue")) {
      currentValue = tempJson.get("stringValue").getAsString();
    }
    if (currentValue == null) {
      throw new IllegalArgumentException("Malformed JSON: " + json);
    }

    // Convert recently-written value to ALL CAPS
    String newValue = currentValue.toUpperCase(Locale.getDefault());

    // Update Firestore DB with ALL CAPS value
    Map<String, String> newFields = Map.of("original", newValue);

    String affectedDoc = context.resource().split("/documents/")[1].replace("\"", "");

    if (!currentValue.equals(newValue)) {
      // The stored value needs to be updated
      // Write the upper-cased value to Firestore
      logger.info(String.format("Replacing value: %s --> %s", currentValue, newValue));
      try {
        FIRESTORE.document(affectedDoc).set(newFields, SetOptions.merge()).get();
      } catch (ExecutionException | InterruptedException e) {
        logger.log(Level.SEVERE, "Error updating Firestore document: " + e.getMessage(), e);
      }
    } else {
      // The stored value is already upper-case, and doesn't need updating.
      // (Don't perform a "second" write, since that could trigger an infinite loop.)
      logger.info(String.format("Value is already upper-case."));
    }
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Firestore;
using Google.Cloud.Functions.Framework;
using Google.Cloud.Functions.Hosting;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace FirestoreReactive
{
    public class Startup : FunctionsStartup
    {
        public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) =>
            services.AddSingleton(FirestoreDb.Create());
    }

    // Register the startup class to provide the Firestore dependency.
    [FunctionsStartup(typeof(Startup))]
    public class Function : ICloudEventFunction<DocumentEventData>
    {
        private readonly ILogger _logger;
        private readonly FirestoreDb _firestoreDb;

        public Function(ILogger<Function> logger, FirestoreDb firestoreDb) =>
            (_logger, _firestoreDb) = (logger, firestoreDb);

        public async Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
        {
            // Get the recently-written value. This expression will result in a null value
            // if any of the following is true:
            // - The event doesn't contain a "new" document
            // - The value doesn't contain a field called "original"
            // - The "original" field isn't a string
            string currentValue = data.Value?.ConvertFields().GetValueOrDefault("original") as string;
            if (currentValue is null)
            {
                _logger.LogWarning($"Event did not contain a suitable document");
                return;
            }

            string newValue = currentValue.ToUpperInvariant();
            if (newValue == currentValue)
            {
                _logger.LogInformation("Value is already upper-cased; no replacement necessary");
                return;
            }

            // The CloudEvent subject is "documents/x/y/...".
            // The Firestore SDK FirestoreDb.Document method expects a reference relative to
            // "documents" (so just the "x/y/..." part). This may be simplified over time.
            if (cloudEvent.Subject is null || !cloudEvent.Subject.StartsWith("documents/"))
            {
                _logger.LogWarning("CloudEvent subject is not a document reference.");
                return;
            }
            string documentPath = cloudEvent.Subject.Substring("documents/".Length);

            _logger.LogInformation("Replacing '{current}' with '{new}' in '{path}'", currentValue, newValue, documentPath);
            await _firestoreDb.Document(documentPath).UpdateAsync("original", newValue);
        }
    }
}

Déployer votre fonction

La commande gcloud suivante déploie une fonction qui est déclenchée par des événements d'écriture sur le document /messages/{pushId} :

gcloud functions deploy FUNCTION_NAME \
  --runtime RUNTIME \
  --trigger-event providers/cloud.firestore/eventTypes/document.write \
  --trigger-resource projects/YOUR_PROJECT_ID/databases/(default)/documents/messages/{pushId}
Argument Description
--runtime RUNTIME Nom de l'environnement d'exécution que vous utilisez. Pour obtenir la liste complète, consultez la documentation de référence sur gcloud.
--trigger-event NAME Type d'événement surveillé par la fonction (write, create, update ou delete).
--trigger-resource NAME Chemin d'accès complet de la base de données sur lequel la fonction écoutera. Il doit être conforme au format suivant : projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH. Le texte {pushId} est un paramètre de caractère générique décrit ci-dessus à la section Spécifier le chemin d'accès du document.

Limitations et garanties

Les déclencheurs Firestore pour Cloud Functions sont en version bêta et présentent des limitations connues :

  • Un délai de 10 secondes peut être nécessaire pour qu'une fonction tienne compte des modifications apportées dans Firestore.
  • 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.
  • Les déclencheurs Firestore pour Cloud Functions ne sont disponibles que pour Firestore en mode natif. La fonctionnalité n'est pas disponible pour Firestore en mode Datastore.