Expanda o Cloud Run com acionadores de eventos através das funções do Cloud Run

Com as funções do Cloud Run, pode implementar código para processar eventos acionados por alterações na sua base de dados do Cloud Run. Isto permite-lhe adicionar funcionalidade do lado do servidor sem executar os seus próprios servidores.

Este guia descreve como criar acionadores para funções do Cloud Run a partir de eventos do Firestore.

Pode acionar as suas funções do Cloud Run a partir de eventos numa base de dados do Firestore. Quando acionada, a sua função lê e atualiza uma base de dados do Firestore em resposta a estes eventos através das APIs e bibliotecas de cliente do Firestore.

O processo de eventos do Firestore que acionam uma função do Cloud Run consiste nos seguintes passos:

  1. O serviço aguarda alterações a um documento específico.

  2. Quando ocorre uma alteração, o serviço é acionado e executa as respetivas tarefas.

  3. O serviço recebe um objeto de dados com um instantâneo 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 de acionamento.

Antes de começar

  1. Certifique-se de que configurou um novo projeto para o Cloud Run, conforme descrito na página de configuração.
  2. Ative as APIs Artifact Registry, Cloud Build, Cloud Run Admin, Eventarc, Firestore Cloud Logging e Pub/Sub:

    Ative as APIs

Funções necessárias

Você ou o seu administrador têm de conceder a identidade da conta de implementação e do acionador. Opcionalmente, conceda ao agente do serviço Pub/Sub as seguintes funções do IAM.

Funções necessárias para a conta do implementador

Para receber as autorizações de que precisa para acionar a partir de eventos do Firestore, peça ao seu administrador para lhe conceder as seguintes funções de IAM no seu projeto:

Para mais informações sobre a atribuição de funções, consulte o artigo Faça a gestão do acesso a projetos, pastas e organizações.

Também pode conseguir as autorizações necessárias através de funções personalizadas ou outras funções predefinidas.

Tenha em atenção que, por predefinição, as autorizações do Cloud Build incluem autorizações para carregar e transferir artefactos doArtifact Registry.

Funções necessárias para a identidade do acionador

  1. Tome nota da conta de serviço predefinida do Compute Engine, uma vez que a vai anexar a um acionador do Eventarc para representar a identidade do acionador para fins de teste. Esta conta de serviço é criada automaticamente depois de ativar ou usar um Google Cloud serviço que usa o Compute Engine e com o seguinte formato de email:

    PROJECT_NUMBER-compute@developer.gserviceaccount.com

    Substitua PROJECT_NUMBER pelo seu Google Cloud número do projeto. Pode encontrar o número do projeto na página Boas-vindas da Google Cloud consola ou executando o seguinte comando:

    gcloud projects describe PROJECT_ID --format='value(projectNumber)'

    Para ambientes de produção, recomendamos vivamente que crie uma nova conta de serviço e lhe conceda uma ou mais funções do IAM que contenham as autorizações mínimas necessárias e siga o princípio do privilégio mínimo.

  2. Por predefinição, os serviços do Cloud Run só podem ser chamados por proprietários do projeto, editores do projeto e administradores e invocadores do Cloud Run. Pode controlar o acesso por serviço. No entanto, para fins de teste, conceda a função de invocador do Cloud Run (run.invoker) no Google Cloud projeto à conta de serviço do Compute Engine. Isto concede a função a todos os serviços e trabalhos do Cloud Run num projeto.
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/run.invoker

    Tenha em atenção que, se criar um acionador para um serviço do Cloud Run autenticado sem conceder a função de invocador do Cloud Run, o acionador é criado com êxito e está ativo. No entanto, o acionador não funciona conforme esperado e é apresentada uma mensagem semelhante à seguinte nos registos:

    The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header.
  3. Conceda a função de recetor de eventos do Eventarc (roles/eventarc.eventReceiver) no projeto à conta de serviço predefinida do Compute Engine para que o acionador do Eventarc possa receber eventos de fornecedores de eventos.
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/eventarc.eventReceiver

Função opcional para o agente do serviço Pub/Sub

  • Se ativou o agente do serviço Cloud Pub/Sub a 8 de abril de 2021 ou antes, para suportar pedidos de envio autenticados do Pub/Sub, conceda a função de criador de tokens de conta de serviço (roles/iam.serviceAccountTokenCreator) ao agente do serviço. Caso contrário, esta função é concedida por predefinição:
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
        --role=roles/iam.serviceAccountTokenCreator

Configure a base de dados do Firestore

Antes de implementar o seu serviço, tem de criar uma base de dados do Firestore:

  1. Aceda à página do Firestore.

  2. Selecione Criar uma base de dados do Firestore.

  3. No campo Dê um nome à sua base de dados, introduza um ID da base de dados, como firestore-db.

  4. Na secção Opções de configuração, a opção Nativo do Firestore é selecionada por predefinição juntamente com as regras de segurança aplicáveis.

  5. Em Tipo de localização, selecione Região e escolha a região onde a sua base de dados vai residir. Esta escolha é permanente.

  6. Clique em Criar base de dados.

O modelo de dados do Firestore consiste em coleções que contêm documentos. Um documento contém um conjunto de pares de chave-valor.

Escreva uma função acionada pelo Firestore

Para escrever uma função que responda a eventos do Firestore, prepare-se para especificar o seguinte durante a implementação:

Tipos de eventos

O Firestore suporta eventos create, update, delete e write. O evento write abrange todas as modificações a um documento.

Tipo de evento Acionador
google.cloud.firestore.document.v1.created (predefinição) Acionado quando um documento é escrito pela primeira vez.
google.cloud.firestore.document.v1.updated Acionado quando já existe um documento e qualquer valor é alterado.
google.cloud.firestore.document.v1.deleted Acionado quando um documento com dados é eliminado.
google.cloud.firestore.document.v1.written Acionado quando um documento é criado, atualizado ou eliminado.
google.cloud.firestore.document.v1.created.withAuthContext Igual a created, mas adiciona informações de autenticação.
google.cloud.firestore.document.v1.updated.withAuthContext Igual a updated, mas adiciona informações de autenticação.
google.cloud.firestore.document.v1.deleted.withAuthContext Igual a deleted, mas adiciona informações de autenticação.
google.cloud.firestore.document.v1.written.withAuthContext Igual a written, mas adiciona informações de autenticação.

Os carateres universais são escritos em acionadores com chavetas, por exemplo: projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

Filtros de eventos de acionador

Para acionar o seu serviço, especifique um caminho do documento para ouvir. O caminho do documento tem de estar no mesmo Google Cloud projeto que o serviço.

Seguem-se alguns exemplos de caminhos de documentos válidos:

  • users/marie: Monitoriza um único documento, /users/marie.

  • users/{username}: monitoriza todos os documentos dos utilizadores. Os carateres universais são usados para monitorizar todos os documentos na coleção.

  • users/{username}/addresses/home: monitoriza o documento de morada de casa para todos os utilizadores.

  • users/{username}/addresses/{addressId}: monitoriza todos os documentos de morada.

  • users/{user=**}: monitoriza todos os documentos de utilizadores e quaisquer documentos em subcoleções em cada documento de utilizador, como /users/userID/address/home ou /users/userID/phone/work.

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

Carateres universais e parâmetros

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

  • users/{username} ouve as alterações a todos os documentos do utilizador.

Neste exemplo, quando qualquer campo em qualquer documento em users é alterado, corresponde a um caráter universal denominado {username}.

Se um documento em users tiver subcoleções e um campo num dos documentos dessas subcoleções for alterado, o {username} caráter universal não é acionado. Se o seu objetivo for responder a eventos em subcoleções, use o caráter universal de vários segmentos {username=**}.

As correspondências com carateres universais são extraídas dos caminhos dos documentos. Pode definir quantos carateres universais quiser para substituir IDs de documentos ou coleções explícitos. Pode usar até um caráter universal de vários segmentos, como {username=**}.

Código da função

Consulte exemplos de como usar eventos do Firestore no modo nativo para acionar uma função do Cloud Run.

Inclua as dependências proto na sua origem

Tem de incluir o ficheiro Cloud Run data.proto no diretório de origem da sua função. Este ficheiro importa os seguintes protos, que também tem de incluir no diretório de origem:

Use a mesma estrutura de diretórios para as dependências. Por exemplo, coloque struct.proto em google/protobuf.

Estes ficheiros são necessários para descodificar os dados de eventos. Se a origem da função não incluir estes ficheiros, devolve um erro quando é executada.

Atributos de eventos

Cada evento inclui atributos de dados que incluem informações sobre o evento, como a hora em que o evento foi acionado. O Cloud Run adiciona dados adicionais sobre a base de dados e o documento envolvidos no evento. Pode aceder a estes atributos da seguinte forma:

Java
logger.info("Function triggered by event on: " + event.getSource());
logger.info("Event type: " + event.getType());
logger.info("Event time " + event.getTime());
logger.info("Event project: " + event.getExtension("project"));
logger.info("Event location: " + event.getExtension("location"));
logger.info("Database name: " + event.getExtension("database"));
logger.info("Database document: " + event.getExtension("document"));
// For withAuthContext events
logger.info("Auth information: " + event.getExtension("authid"));
logger.info("Auth information: " + event.getExtension("authtype"));
Node.js
console.log(`Function triggered by event on: ${cloudEvent.source}`);
console.log(`Event type: ${cloudEvent.type}`);
console.log(`Event time: ${cloudEvent.time}`);
console.log(`Event project: ${cloudEvent.project}`);
console.log(`Event location: ${cloudEvent.location}`);
console.log(`Database name: ${cloudEvent.database}`);
console.log(`Document name: ${cloudEvent.document}`);
// For withAuthContext events
console.log(`Auth information: ${cloudEvent.authid}`);
console.log(`Auth information: ${cloudEvent.authtype}`);
Python
print(f"Function triggered by change to: {cloud_event['source']}")
print(f"Event type: {cloud_event['type']}")
print(f"Event time: {cloud_event['time']}")
print(f"Event project: {cloud_event['project']}")
print(f"Location: {cloud_event['location']}")
print(f"Database name: {cloud_event['database']}")
print(f"Document: {cloud_event['document']}")
// For withAuthContext events
print(f"Auth information: {cloud_event['authid']}")
print(f"Auth information: {cloud_event['authtype']}")

Estruturas de eventos

Este acionador invoca o seu serviço com um evento semelhante ao seguinte:

{
    "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 Value documentação para referências de tipos.

Crie acionadores para funções

Clique no separador para ver instruções sobre como usar a ferramenta da sua escolha.

Consola

Quando usa a Google Cloud consola para criar uma função, também pode adicionar um acionador à sua função. Siga estes passos para criar um acionador para a sua função:

  1. Na Google Cloud consola, aceda ao Cloud Run:

    Aceda ao Cloud Run

  2. Clique em Escrever uma função e introduza os detalhes da função. Para mais informações sobre a configuração de funções durante a implementação, consulte Implementar funções.

  3. Na secção Acionador, clique em Adicionar acionador.

  4. Selecione Acionador do Firestore.

  5. No painel Acionador do Eventarc, modifique os detalhes do acionador da seguinte forma:

    1. Introduza um nome para o acionador no campo Nome do acionador ou use o nome predefinido.

    2. Selecione um Tipo de acionador na lista:

      • Origens Google para especificar acionadores para o Pub/Sub, o Cloud Storage, o Firestore e outros fornecedores de eventos Google.

      • Terceiros para integrar com fornecedores que não sejam da Google que oferecem uma origem do Eventarc. Para mais informações, consulte o artigo Eventos de terceiros no Eventarc.

    3. Selecione Firestore na lista Fornecedor de eventos para selecionar um produto que forneça o tipo de evento para acionar a sua função. Para ver a lista de fornecedores de eventos, consulte o artigo Fornecedores de eventos e destinos.

    4. Selecione type=google.cloud.firestore.document.v1.created na lista Tipo de evento. A configuração do acionador varia consoante o tipo de evento suportado. Para mais informações, consulte o artigo Tipos de eventos.

    5. Na secção Filtros, selecione uma base de dados, uma operação e valores de atributos, ou use as seleções predefinidas.

    6. Se o campo Região estiver ativado, selecione uma localização para o acionador do Eventarc. Em geral, a localização de um acionador do Eventarc deve corresponder à localização do recurso que quer monitorizar para eventos. Google Cloud Na maioria dos cenários, também deve implementar a função na mesma região. Consulte o artigo Compreenda as localizações do Eventarc para ver mais detalhes sobre as localizações dos acionadores do Eventarc.

    7. No campo Conta de serviço, selecione uma conta de serviço. Os acionadores do Eventarc estão associados a contas de serviço para usar como identidade quando invocam a sua função. A conta de serviço do acionador do Eventarc tem de ter a autorização para invocar a sua função. Por predefinição, o Cloud Run usa a conta de serviço predefinida do Compute Engine.

    8. Opcionalmente, especifique o caminho do URL do serviço para o qual enviar o pedido recebido. Este é o caminho relativo no serviço de destino para o qual os eventos do acionador devem ser enviados. Por exemplo: /, /route, route e route/subroute.

  6. Depois de preencher os campos obrigatórios, clique em Guardar acionador.

gcloud

Quando cria uma função através da CLI gcloud, tem primeiro de implementar a função e, em seguida, criar um acionador. Siga estes passos para criar um acionador para a sua função:

  1. Execute o seguinte comando no diretório que contém o código de exemplo para implementar a sua função:

    gcloud run deploy FUNCTION \
            --source . \
            --function FUNCTION_ENTRYPOINT \
            --base-image BASE_IMAGE_ID \
            --region REGION
    

    Substituição:

    • FUNCTION com o nome da função que está a implementar. Pode omitir este parâmetro por completo, mas é-lhe pedido o nome se o omitir.

    • FUNCTION_ENTRYPOINT com o ponto de entrada da sua função no código-fonte. Este é o código que o Cloud Run executa quando a sua função é executada. O valor desta flag tem de ser um nome de função ou um nome de classe totalmente qualificado que exista no seu código-fonte.

    • BASE_IMAGE_ID com o ambiente de imagem base para a sua função. Para mais detalhes sobre as imagens base e os pacotes incluídos em cada imagem, consulte o artigo Imagens base de tempos de execução.

    • REGION com a Google Cloud região onde quer implementar a sua função. Por exemplo, europe-west1.

  2. Execute o seguinte comando para criar um acionador que filtra eventos:

    gcloud eventarc triggers create TRIGGER_NAME  \
        --location=EVENTARC_TRIGGER_LOCATION \
        --destination-run-service=FUNCTION  \
        --destination-run-region=REGION \
        --event-filters="type=google.cloud.firestore.document.v1.created" \
        --service-account=PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    Substituição:

    • TRIGGER_NAME com o nome do acionador.

    • EVENTARC_TRIGGER_LOCATION com a localização do acionador do Eventarc. Em geral, a localização de um acionador do Eventarc deve corresponder à localização do Google Cloud recurso que quer monitorizar para eventos. Na maioria dos cenários, também deve implementar a função na mesma região. Para mais informações, consulte o artigo Localizações do Eventarc.

    • FUNCTION com o nome da função que está a implementar.

    • REGION com a região do Cloud Run da função.

    • PROJECT_NUMBER com o seu Google Cloud número do projeto. Os acionadores do Eventarc estão associados a contas de serviço para serem usados como uma identidade quando invocam a sua função. A conta de serviço do seu acionador do Eventarc tem de ter autorização para invocar a sua função. Por predefinição, o Cloud Run usa a conta de serviço de computação predefinida.

    Cada sinalização event-filters especifica um tipo de evento, com a função a ser acionada apenas quando um evento cumpre todos os critérios especificados nas respetivas sinalizações event-filters. Cada acionador tem de ter uma flag event-filters que especifique um tipo de evento suportado, como um novo documento escrito no Firestore ou um ficheiro carregado para o Cloud Storage. Não pode alterar o tipo de filtro de eventos após a criação. Para alterar o tipo de filtro de eventos, tem de criar um novo acionador e eliminar o antigo. Opcionalmente, pode repetir a flag --event-filters com um filtro suportado no formato ATTRIBUTE=VALUE para adicionar mais filtros.

Terraform

Para criar um acionador do Eventarc para uma função do Cloud Run, consulte o artigo Crie um acionador com o Terraform.

Exemplos

Os exemplos seguintes descrevem como usar eventos do Firestore no modo nativo para acionar uma função do Cloud Run.

Exemplo 1: função Hello Firestore

O exemplo seguinte imprime os campos de um evento do Firestore de acionamento:

Node.js

/**
 * Cloud Event Function triggered by a change to a Firestore document.
 */
const functions = require('@google-cloud/functions-framework');
const protobuf = require('protobufjs');

functions.cloudEvent('helloFirestore', async cloudEvent => {
  console.log(`Function triggered by event on: ${cloudEvent.source}`);
  console.log(`Event type: ${cloudEvent.type}`);

  console.log('Loading protos...');
  const root = await protobuf.load('data.proto');
  const DocumentEventData = root.lookupType(
    'google.events.cloud.firestore.v1.DocumentEventData'
  );

  console.log('Decoding data...');
  const firestoreReceived = DocumentEventData.decode(cloudEvent.data);

  console.log('\nOld value:');
  console.log(JSON.stringify(firestoreReceived.oldValue, null, 2));

  console.log('\nNew value:');
  console.log(JSON.stringify(firestoreReceived.value, null, 2));
});

Python

from cloudevents.http import CloudEvent
import functions_framework
from google.events.cloud import firestore


@functions_framework.cloud_event
def hello_firestore(cloud_event: CloudEvent) -> None:
    """Triggers by a change to a Firestore document.

    Args:
        cloud_event: cloud event with information on the firestore event trigger
    """
    firestore_payload = firestore.DocumentEventData()
    firestore_payload._pb.ParseFromString(cloud_event.data)

    print(f"Function triggered by change to: {cloud_event['source']}")

    print("\nOld value:")
    print(firestore_payload.old_value)

    print("\nNew value:")
    print(firestore_payload.value)

Go


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

import (
	"context"
	"fmt"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
	"github.com/googleapis/google-cloudevents-go/cloud/firestoredata"
	"google.golang.org/protobuf/proto"
)

func init() {
	functions.CloudEvent("helloFirestore", HelloFirestore)
}

// HelloFirestore is triggered by a change to a Firestore document.
func HelloFirestore(ctx context.Context, event event.Event) error {
	var data firestoredata.DocumentEventData

	// If you omit `DiscardUnknown`, protojson.Unmarshal returns an error
	// when encountering a new or unknown field.
	options := proto.UnmarshalOptions{
		DiscardUnknown: true,
	}
	err := options.Unmarshal(event.Data(), &data)

	if err != nil {
		return fmt.Errorf("proto.Unmarshal: %w", err)
	}

	fmt.Printf("Function triggered by change to: %v\n", event.Source())
	fmt.Printf("Old value: %+v\n", data.GetOldValue())
	fmt.Printf("New value: %+v\n", data.GetValue())
	return nil
}

Java

import com.google.cloud.functions.CloudEventsFunction;
import com.google.events.cloud.firestore.v1.DocumentEventData;
import com.google.protobuf.InvalidProtocolBufferException;
import io.cloudevents.CloudEvent;
import java.util.logging.Logger;

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

  @Override
  public void accept(CloudEvent event) throws InvalidProtocolBufferException {
    DocumentEventData firestoreEventData = DocumentEventData
        .parseFrom(event.getData().toBytes());

    logger.info("Function triggered by event on: " + event.getSource());
    logger.info("Event type: " + event.getType());

    logger.info("Old value:");
    logger.info(firestoreEventData.getOldValue().toString());

    logger.info("New value:");
    logger.info(firestoreEventData.getValue().toString());
  }
}

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));
    }
}

Implemente a função

Para implementar a função Hello Firestore, execute o seguinte comando:

Se ainda não o fez, configure a sua base de dados do Firestore.

Para implementar a função, consulte o artigo Crie acionadores para funções.

Teste a função

Para testar a função Hello Firestore, configure uma coleção denominada users na sua base de dados do Firestore:

  1. Na Google Cloud consola, aceda à página de bases de dados do Firestore:

    Aceder ao Firestore

  2. Clique em Iniciar uma coleção.

  3. Especifique users como o ID da coleção.

  4. Para começar a adicionar o primeiro documento da coleção, em Adicionar o primeiro documento, aceite o ID do documento gerado automaticamente.

  5. Adicione, pelo menos, um campo para o documento, especificando um nome e um valor. Por exemplo, em Nome do campo, introduza username e, em Valor do campo, introduza rowan.

  6. Quando terminar, clique em Guardar.

    Esta ação cria um novo documento, acionando assim a sua função.

  7. Para confirmar que a sua função foi acionada, clique no nome associado da função na Google Cloud consola página Vista geral do Cloud Run para abrir a página Detalhes do serviço.

  8. Selecione o separador Registos e procure esta string:

Function triggered by change to: //firestore.googleapis.com/projects/your-project-id/databases/(default)'

Exemplo 2: função de conversão em maiúsculas

O exemplo seguinte obtém o valor adicionado pelo utilizador, converte a string nessa localização em letras maiúsculas e substitui o valor pela string em letras maiúsculas:

Node.js

Use protobufjs para descodificar os dados do evento. Inclua o google.events.cloud.firestore.v1 data.proto na sua fonte.

const functions = require('@google-cloud/functions-framework');
const Firestore = require('@google-cloud/firestore');
const protobuf = require('protobufjs');

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

// Converts strings added to /messages/{pushId}/original to uppercase
functions.cloudEvent('makeUpperCase', async cloudEvent => {
  console.log('Loading protos...');
  const root = await protobuf.load('data.proto');
  const DocumentEventData = root.lookupType(
    'google.events.cloud.firestore.v1.DocumentEventData'
  );

  console.log('Decoding data...');
  const firestoreReceived = DocumentEventData.decode(cloudEvent.data);

  const resource = firestoreReceived.value.name;
  const affectedDoc = firestore.doc(resource.split('/documents/')[1]);

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

  if (curValue === newValue) {
    // Value is already upper-case
    // Don't perform a(nother) write to avoid infinite loops
    console.log('Value is already upper-case.');
    return;
  }

  console.log(`Replacing value: ${curValue} --> ${newValue}`);
  affectedDoc.set({
    original: newValue,
  });
});

Python

from cloudevents.http import CloudEvent
import functions_framework
from google.cloud import firestore
from google.events.cloud import firestore as firestoredata

client = firestore.Client()


# Converts strings added to /messages/{pushId}/original to uppercase
@functions_framework.cloud_event
def make_upper_case(cloud_event: CloudEvent) -> None:
    firestore_payload = firestoredata.DocumentEventData()
    firestore_payload._pb.ParseFromString(cloud_event.data)

    path_parts = firestore_payload.value.name.split("/")
    separator_idx = path_parts.index("documents")
    collection_path = path_parts[separator_idx + 1]
    document_path = "/".join(path_parts[(separator_idx + 2) :])

    print(f"Collection path: {collection_path}")
    print(f"Document path: {document_path}")

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

    cur_value = firestore_payload.value.fields["original"].string_value
    new_value = cur_value.upper()

    if cur_value != new_value:
        print(f"Replacing value: {cur_value} --> {new_value}")
        affected_doc.set({"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"
	"errors"
	"fmt"
	"log"
	"os"
	"strings"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go/v4"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
	"github.com/googleapis/google-cloudevents-go/cloud/firestoredata"
	"google.golang.org/protobuf/proto"
)

// set the GOOGLE_CLOUD_PROJECT environment variable when deploying.
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)
	}

	// Register cloud event function
	functions.CloudEvent("MakeUpperCase", MakeUpperCase)
}

// 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 event.Event) error {
	var data firestoredata.DocumentEventData

	// If you omit `DiscardUnknown`, protojson.Unmarshal returns an error
	// when encountering a new or unknown field.
	options := proto.UnmarshalOptions{
		DiscardUnknown: true,
	}
	err := options.Unmarshal(e.Data(), &data)

	if err != nil {
		return fmt.Errorf("proto.Unmarshal: %w", err)
	}

	if data.GetValue() == nil {
		return errors.New("Invalid message: 'Value' not present")
	}

	fullPath := strings.Split(data.GetValue().GetName(), "/documents/")[1]
	pathParts := strings.Split(fullPath, "/")
	collection := pathParts[0]
	doc := strings.Join(pathParts[1:], "/")

	var originalStringValue string
	if v, ok := data.GetValue().GetFields()["original"]; ok {
		originalStringValue = v.GetStringValue()
	} else {
		return errors.New("Document did not contain field \"original\"")
	}

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

	newDocumentEntry := map[string]string{"original": newValue}
	_, err = client.Collection(collection).Doc(doc).Set(ctx, newDocumentEntry)
	if err != nil {
		return fmt.Errorf("Set: %w", 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.CloudEventsFunction;
import com.google.events.cloud.firestore.v1.DocumentEventData;
import com.google.events.cloud.firestore.v1.Value;
import com.google.protobuf.InvalidProtocolBufferException;
import io.cloudevents.CloudEvent;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.logging.Logger;

public class FirebaseFirestoreReactive implements CloudEventsFunction {
  private static final Logger logger = Logger.getLogger(FirebaseFirestoreReactive.class.getName());
  private final Firestore firestore;

  private static final String FIELD_KEY = "original";
  private static final String APPLICATION_PROTOBUF = "application/protobuf";

  public FirebaseFirestoreReactive() {
    this(FirestoreOptions.getDefaultInstance().getService());
  }

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

  @Override
  public void accept(CloudEvent event)
      throws InvalidProtocolBufferException, InterruptedException, ExecutionException {
    if (event.getData() == null) {
      logger.warning("No data found in event!");
      return;
    }

    if (!event.getDataContentType().equals(APPLICATION_PROTOBUF)) {
      logger.warning(String.format("Found unexpected content type %s, expected %s",
          event.getDataContentType(),
          APPLICATION_PROTOBUF));
      return;
    }

    DocumentEventData firestoreEventData = DocumentEventData
        .parseFrom(event.getData().toBytes());

    // Get the fields from the post-operation document snapshot
    // https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents#Document
    Map<String, Value> fields = firestoreEventData.getValue().getFieldsMap();
    if (!fields.containsKey(FIELD_KEY)) {
      logger.warning("Document does not contain original field");
      return;
    }
    String currValue = fields.get(FIELD_KEY).getStringValue();
    String newValue = currValue.toUpperCase();

    if (currValue.equals(newValue)) {
      logger.info("Value is already upper-case");
      return;
    }

    // Retrieve the document name from the resource path:
    // projects/{project_id}/databases/{database_id}/documents/{document_path}
    String affectedDoc = firestoreEventData.getValue()
        .getName()
        .split("/documents/")[1]
        .replace("\"", "");

    logger.info(String.format("Replacing values: %s --> %s", currValue, newValue));

    // Wait for the async call to complete
    this.firestore
        .document(affectedDoc)
        .set(Map.of(FIELD_KEY, newValue), SetOptions.merge())
        .get();
  }
}

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, cancellationToken: cancellationToken);
    }
}

Implemente a função

Para implementar a função Convert to Uppercase, execute o seguinte comando:

Se ainda não o fez, configure a sua base de dados do Firestore.

Para implementar a função, consulte o artigo Crie acionadores para funções.

Teste a função

Para testar a função Convert to Uppercase que acabou de implementar, configure uma coleção denominada messages na sua base de dados do Firestore:

  1. Na Google Cloud consola, aceda à página de bases de dados do Firestore:

    Aceder ao Firestore

  2. Clique em Iniciar uma coleção.

  3. Especifique messages como o ID da coleção.

  4. Para começar a adicionar o primeiro documento da coleção, em Adicionar o primeiro documento, aceite o ID do documento gerado automaticamente.

  5. Para acionar a função implementada, adicione um documento em que o Nome do campo seja original e o Valor do campo seja minka.

  6. Quando guarda o documento, pode ver a palavra em minúsculas no campo de valor convertida em maiúsculas.

    Se, posteriormente, editar o valor do campo para incluir letras minúsculas, isso aciona novamente a função, convertendo todas as letras minúsculas em maiúsculas.

Limitações para funções

  • A ordenação não é garantida. As alterações rápidas podem acionar invocações de funções numa ordem inesperada.
  • Os eventos são entregues, pelo menos, uma vez, mas um único evento pode resultar em várias invocações de funções. Evite depender de mecanismos de execução única e escreva funções idempotentes.
  • Um acionador está associado a uma única base de dados. Não pode criar um acionador que corresponda a várias bases de dados.
  • A eliminação de uma base de dados não elimina automaticamente os acionadores dessa base de dados. O acionador deixa de enviar eventos, mas continua a existir até o eliminar.