Activadores de Google Cloud Firestore

Cloud Functions puede manejar eventos en Cloud Firestore sin necesidad de actualizar el código de cliente. Puedes leer o actualizar Cloud Firestore en respuesta a estos eventos mediante las API de Firestore y las bibliotecas cliente.

En un ciclo de vida típico, una función de Cloud Firestore realiza las siguientes acciones:

  1. Espera a que ocurran cambios en un documento en particular.

  2. Se activa cuando ocurre un evento y realiza sus tareas.

  3. Recibe un objeto de datos con una instantánea del documento afectado. En el caso de los eventos write o update, el objeto de datos contiene instantáneas que representan el estado del documento antes y después del evento de activación.

Tipos de eventos

Cloud Firestore admite los eventos create, update, delete y write. El evento write comprende todas las modificaciones que se realizan a un documento.

Tipo de evento Activador
providers/cloud.firestore/eventTypes/document.create Se activa cuando se escribe en un documento por primera vez.
providers/cloud.firestore/eventTypes/document.update Se activa cuando un documento ya existe y se cambia uno de sus valores.
providers/cloud.firestore/eventTypes/document.delete Se activa cuando se borra un documento con datos.
providers/cloud.firestore/eventTypes/document.write Se activa cuando se crea, actualiza o borra un documento.

Los comodines se escriben en los activadores con llaves, como se muestra en el siguiente ejemplo: /projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

Especifica la ruta de acceso del documento

Para activar la función, especifica la ruta de acceso del documento que deseas escuchar. Las funciones solo responden a los cambios del documento y no pueden supervisar colecciones o campos específicos. A continuación, se muestran algunos ejemplos de rutas de acceso de documentos válidas:

  • users/marie: activador válido. Supervisa un solo documento, /users/marie.

  • users/{username}: activador válido. Supervisa todos los documentos del usuario. Los comodines se usan para supervisar todos los documentos de la colección.

  • users/{username}/addresses: activador no válido. Se refiere a la subcolección addresses, no a un documento.

  • users/{username}/addresses/home: activador válido. Supervisa el documento de dirección personal de todos los usuarios.

  • users/{username}/addresses/{addressId}: activador válido. Supervisa todos los documentos de dirección.

Usa comodines y parámetros

Si no conoces el documento específico que deseas supervisar, usa un {wildcard} en lugar del ID del documento:

  • users/{username} escucha cambios en todos los documentos del usuario.

En este ejemplo, cuando se cambia cualquier campo en cualquier documento de users, coincide con un comodín llamado {username}.

Si un documento de users tiene subcolecciones y se modifica un campo de uno de los documentos en ellas, no se activará el comodín {username}.

Las coincidencias de comodines se extraen de las rutas de acceso de documentos y se guardan en event.params. Puedes definir tantos comodines como desees para sustituir los ID explícitos de colección o documento.

Estructura de eventos

Este activador invoca tu función con un evento similar al que se muestra en el siguiente ejemplo:

{
    "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
    }
}

Cada objeto Document contiene uno o más objetos Value. Consulta la documentación de Value para obtener referencias de tipo. Esto resulta muy útil si usas un lenguaje escrito (como Go) para escribir tus funciones.

Muestra de código

La siguiente muestra de Cloud Functions imprime los campos de un evento de activación de Cloud Firestore:

Node.js 8/10

/**
 * Triggered by a change to a Firestore document.
 *
 * @param {object} data The event payload.
 * @param {object} context The event metadata.
 */
exports.helloFirestore = (data, context) => {
  const triggerResource = context.resource;

  console.log(`Function triggered by change to: ${triggerResource}`);
  console.log(`Event type: ${context.eventType}`);

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

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

Node.js 6 (obsoleta)

/**
 * 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.data.oldValue && Object.keys(event.data.oldValue).length) {
    console.log(`\nOld value:`);
    console.log(JSON.stringify(event.data.oldValue, null, 2));
  }

  if (event.data.value && Object.keys(event.data.value).length) {
    console.log(`\nNew value:`);
    console.log(JSON.stringify(event.data.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
}

En el siguiente ejemplo, se recupera el valor que agrega el usuario, se convierte la string en esa ubicación en mayúscula y se reemplaza el valor por la string en mayúscula:

Node.js 8/10

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

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

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

  const curValue = data.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();
  console.log(`Replacing value: ${curValue} --> ${newValue}`);

  return affectedDoc.set({
    original: newValue,
  });
};

Node.js 6 (obsoleta)

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

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

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

  const curValue = event.data.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();
  console.log(`Replacing value: ${curValue} --> ${newValue}`);

  return affectedDoc.set({
    original: newValue,
  });
};

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()
    print(f'Replacing value: {cur_value} --> {new_value}')

    affected_doc.set({
        u'original': new_value
    })

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"
)

// 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"`
}

// GCLOUD_PROJECT is automatically set by the Cloud Functions runtime.
var projectID = os.Getenv("GCLOUD_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
}

Implementa la función

El siguiente comando de gcloud implementa una función que se activa mediante eventos de escritura en el documento /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}
Argumento Descripción
--runtime RUNTIME El nombre del entorno de ejecución que usas. Para obtener una lista completa, consulta la referencia de gcloud.
--trigger-event NAME El tipo de evento que supervisará la función (uno entre write, create, update o delete).
--trigger-resource NAME Es la ruta de acceso de la base de datos completamente calificada a la que escuchará la función. Esto debe cumplir con el siguiente formato: projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH El texto {pushId} es un parámetro comodín, como se describe antes en la sección Especifica la ruta de acceso del documento.

Limitaciones y garantías

Los activadores de Cloud Firestore para Cloud Functions son una función Beta con algunas limitaciones conocidas, como las que se describen a continuación:

  • Una función puede tardar hasta 10 segundos en responder a los cambios en Cloud Firestore.
  • No se garantiza el ordenamiento. Los cambios rápidos pueden activar invocaciones de funciones en un orden inesperado.
  • Los eventos se entregan al menos una vez, pero un solo evento puede dar lugar a varias invocaciones de funciones. Evita depender de la mecánica de entrega de eventos solo una vez y escribe funciones idempotentes.
  • Los activadores de Cloud Firestore en Cloud Functions solo están disponibles para Cloud Firestore en modo nativo. No están disponibles para Cloud Firestore en modo Datastore.