Acionadores do Google Cloud Firestore

O Cloud Functions pode manipular eventos no Cloud Firestore sem a necessidade de atualizar o código do cliente. É possível ler e/ou atualizar o Cloud Firestore em resposta a esses eventos usando as APIs do Firestore e as bibliotecas de cliente.

Em um ciclo de vida comum, uma função do Cloud Firestore realiza as seguintes tarefas:

  1. Espera por alterações em um documento específico.

  2. É acionada quando um evento ocorre e realiza as tarefas dele.

  3. Recebe um objeto de dados com um snapshot do documento afetado. Para eventos write ou update, o objeto de dados contém instantâneos que representam o estado do documento antes e depois do evento acionador.

Tipos de evento

Cloud Firestore suporta os eventos create, update, delete e write. O evento write engloba todas as modificações em um documento.

Tipo de evento Acionador
providers/cloud.firestore/eventTypes/document.create Acionado quando um documento é gravado pela primeira vez.
providers/cloud.firestore/eventTypes/document.update Acionado quando um documento já existe e tem algum valor alterado.
providers/cloud.firestore/eventTypes/document.delete Acionado quando um documento com dados é excluído.
providers/cloud.firestore/eventTypes/document.write Acionado quando um documento é criado, atualizado ou excluído.

Os caracteres curingas são escritos em acionadores usando chaves, da seguinte maneira: /projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

Como especificar o caminho do documento

Para acionar sua função, especifique um caminho de documento para detectar. As funções só respondem às alterações do documento e não podem monitorar campos ou conjuntos específicos. Veja a seguir alguns exemplos de caminhos de documentos válidos:

  • users/marie: acionador válido. Monitora um único documento, /users/marie.

  • users/{username}: acionador válido. Monitora todos os documentos do usuário. Caracteres curingas são usados para monitorar todos os documentos na coleção.

  • users/{username}/addresses: acionador inválido. Refere-se à subcoleção addresses, não a um documento.

  • users/{username}/addresses/home: acionador válido. Monitora o documento de endereço residencial de todos os usuários.

  • users/{username}/addresses/{addressId}: acionador válido. Monitora todos os documentos de endereço.

Como usar caracteres curinga e parâmetros

Se você não souber o documento específico que quer monitorar, use um {wildcard} em vez do ID do documento:

  • users/{username} detecta alterações feitas em todos os documentos do usuário.

Neste exemplo, quando qualquer campo em qualquer documento em users é alterado, ele corresponde a um caractere curinga chamado {username}.

Se um documento em users tem subcoleções, e um campo em um dos documentos dessas subcoleções for alterado, o caractere curinga {username} não é acionado.

As correspondências curinga são extraídas dos caminhos do documento e armazenadas em event.params. Defina quantos caracteres curinga você quiser para substituir a coleção explícita ou os códigos dos documentos.

Estrutura do evento

Esse acionador invoca sua função com um evento semelhante ao mostrado abaixo:

{
    "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 contém um ou mais objetos Value. Consulte a documentação Value para referências de tipo. Isso é especialmente útil se você estiver usando uma linguagem tipada (como Go) para escrever suas funções.

Exemplo de código

O Cloud Function exemplificado abaixo imprime os campos de um evento de acionamento do Cloud Firestore:

Node.js 8+

/**
 * 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 (obsoleto)

/**
 * 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
}

O exemplo abaixo recupera o valor adicionado pelo usuário, converte a string nesse local para letras maiúsculas e substitui o valor pela string de caracteres maiúsculos:

Node.js 8+

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 (obsoleto)

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
}

Como implantar a função

O comando gcloud a seguir implanta uma função que é acionada por eventos de gravação no 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 Descrição
--runtime RUNTIME O nome do ambiente de execução que você está usando. Para obter uma lista completa, consulte a referência do gcloud.
--trigger-event NAME O tipo de evento que a função vai monitorar (um de write, create, update ou delete).
--trigger-resource NAME O caminho completo do banco de dados qualificado em que a função vai detectar. Isso deve estar de acordo com o seguinte formato: projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH O texto {pushId} é um parâmetro curinga descrito acima em Especificação do caminho do documento.

Limitações e garantias

Os acionadores do Firestore para Cloud Functions são um recurso beta com algumas limitações conhecidas:

  • Pode levar até 10 segundos para uma função responder a alterações no Firestore.
  • Não garantimos acionamentos em ordem. Alterações rápidas podem acionar invocações de função em uma ordem inesperada.
  • Os eventos são entregues pelo menos uma vez, mas um único evento pode resultar em invocações de várias funções. Evite depender de mecanismos "apenas uma vez" e escreva funções idempotentes.
  • Os acionadores do Firestore para Cloud Functions estão disponíveis somente para o Firestore no modo nativo. Ele não está disponível para o Firestore no modo Datastore.