Ampliar o Cloud Firestore com o Cloud Functions (2ª geração)

Com o Cloud Functions, é possível implantar o código para processar eventos acionados por alterações em seu banco de dados do Cloud Firestore. Isso permite que você adicione funcionalidade do lado do servidor no seu app facilmente, sem executar seus próprios servidores.

Cloud Functions (2ª geração)

Com a tecnologia do Cloud Run e do Eventarc, o Cloud Functions para Firebase (2ª geração) oferece uma infraestrutura mais eficiente, controle avançado sobre o desempenho e a escalonabilidade e mais controle do ambiente de execução das funções. Para mais informações sobre a segunda geração, consulte Cloud Functions para Firebase (2ª geração). Para saber mais sobre a 1ª geração, consulte Ampliar o Cloud Firestore com o Cloud Functions.

Gatilhos da função do Cloud Firestore

O SDK do Cloud Functions para Firebase exporta os seguintes gatilhos de evento do Cloud Firestore para permitir que você crie gerenciadores relacionados a eventos específicos do Cloud Firestore:

Node.js

Tipo de evento Gatilho
onDocumentCreated Acionado quando um documento é gravado pela primeira vez.
onDocumentUpdated Acionado quando um documento já existe e tem algum valor alterado.
onDocumentDeleted Acionado quando um documento é excluído.
onDocumentWritten Acionado quando onDocumentCreated, onDocumentUpdated ou onDocumentDeleted é acionado.

Python (pré-lançamento)

Tipo de evento Gatilho
on_document_created Acionado quando um documento é gravado pela primeira vez.
on_document_updated Acionado quando um documento já existe e tem algum valor alterado.
on_document_deleted Acionado quando um documento é excluído.
on_document_written Acionado quando on_document_created, on_document_updated ou on_document_deleted é acionado.

Os eventos do Cloud Firestore são acionados somente se houver mudanças nos documentos. Uma atualização em um documento do Cloud Firestore, em que os dados permanecem inalterados (uma gravação em ambiente autônomo), não vai gerar um evento de atualização ou gravação. Não é possível adicionar eventos a campos específicos.

Se você ainda não ativou um projeto no Cloud Functions para Firebase, leia Introdução ao Cloud Functions para Firebase (2ª geração) para configurar seu projeto.

Como escrever funções acionadas pelo Cloud Firestore

Definir um gatilho de função

Para definir um gatilho do Cloud Firestore, especifique um caminho do documento e um tipo de evento:

Node.js

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

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

Python (pré-lançamento)

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:

Os caminhos do documento podem se referir a um documento específico ou a um padrão de caracteres curinga.

Especificar um único documento

Se você quiser acionar um evento para qualquer alteração em um documento específico, use a seguinte função.

Node.js

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

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

Python (pré-lançamento)

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:

Especificar um grupo de documentos com caracteres curinga

Para adicionar um gatilho a um grupo de documentos, como qualquer documento em uma determinada coleção, use um {wildcard} no lugar do ID do documento:

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 (pré-lançamento)

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

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

Se um documento em users tiver subcoleções e um campo em um dos documentos dessas subcoleções for alterado, o caractere curinga userId não será acionado.

As correspondências de caracteres curinga são extraídas do caminho do documento e armazenadas em event.params. É possível definir quantos caracteres curinga você quiser para substituir a coleção explícita ou os IDs do documento:

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 (pré-lançamento)

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

É preciso que seu gatilho aponte sempre para um documento, mesmo que você esteja usando um caractere curinga. Por exemplo, users/{userId}/{messageCollectionId} não é válido porque {messageCollectionId} é uma coleção. No entanto, users/{userId}/{messageCollectionId}/{messageId} é válido porque {messageId} sempre vai apontar para um documento.

Gatilhos de eventos

Acionar uma função quando um novo documento é criado

Você pode acionar uma função sempre que um novo documento for criado em uma coleção. Este exemplo de função é acionado sempre que um novo perfil de usuário é adicionado:

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

Python (pré-lançamento)

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

Acionar uma função quando um documento é atualizado

Você também pode acionar uma função quando um documento for atualizado. Esta função de exemplo é acionada quando um perfil de usuário for alterado:

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

Python (pré-lançamento)

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

Acionar uma função quando um documento é excluído

Também é possível acionar uma função quando um documento for excluído. Esta função de exemplo é acionada quando um perfil de usuário for excluído:

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

Python (pré-lançamento)

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

Acionar uma função para todas as alterações em um documento

Se o tipo de evento acionado não for relevante, será possível detectar todas as alterações em um documento do Cloud Firestore usando o acionador de evento "documento gravado". Esta função de exemplo é acionada se um usuário for criado, atualizado ou excluído:

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

Python (pré-lançamento)

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

Como ler e gravar dados

Quando uma função é acionada, ela fornece um snapshot dos dados relacionados ao evento. É possível usar esse snapshot para ler ou gravar no documento usado para acionar o evento ou usar o SDK Admin do Firebase para acessar outras partes do seu banco de dados.

Dados de eventos

Como ler dados

Quando uma função é acionada, é possível que você queira ver os dados de um documento antes ou depois de que ele seja atualizado. Para conseguir os dados anteriores, basta usar event.data.before, que contém o snapshot do documento antes da atualização. Da mesma forma, event.data.after contém o estado do snapshot do documento após a atualização.

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 (pré-lançamento)

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

Acesse as propriedades como faria em qualquer outro objeto. Como alternativa, use a função get para acessar campos específicos:

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 (pré-lançamento)

# 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"]

Como gravar dados

Cada invocação de função está associada a um documento específico no seu banco de dados do Cloud Firestore. É possível acessar esse documento no snapshot retornado para sua função.

A referência do documento inclui métodos como update(), set() e remove() para que você possa modificar o documento que acionou a função.

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 (pré-lançamento)

@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})
 ```

Dados fora do evento acionador

O Cloud Functions é executado em um ambiente confiável. Os dados são autorizados como uma conta de serviço no projeto, e é possível executar leituras e gravações usando o SDK Admin do Firebase:

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 (pré-lançamento)

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({
      # ...
  })

Limitações

Observe as seguintes limitações para os gatilhos do Cloud Firestore para Cloud Functions:

  • 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 do tipo "apenas uma vez" e escreva funções idempotentes.
  • O Cloud Firestore no modo Datastore requer o Cloud Functions (2ª geração). O Cloud Functions (1ª geração) não é compatível com o modo Datastore.
  • O Cloud Functions (1ª geração) só funciona com o banco de dados "(padrão)" e não oferece suporte aos bancos de dados nomeados do Cloud Firestore. Use o Cloud Functions (2ª geração) para configurar eventos para bancos de dados nomeados.
  • Um gatilho está associado a um único banco de dados. Não é possível criar um gatilho que corresponda a vários bancos de dados.
  • A exclusão de um banco de dados não remove automaticamente nenhum gatilho dele. O acionador deixa de entregar eventos, mas continua existindo até que você o exclua.
  • Se um evento correspondente exceder o tamanho máximo da solicitação, ele pode não ser entregue ao Cloud Functions (1ª geração).
    • Os eventos não entregues devido ao tamanho da solicitação são registrados nos registros da plataforma e contabilizados no uso de registros do projeto.
    • É possível encontrar esses registros na Análise de registros com a mensagem "O evento não pode ser entregue à função do Cloud devido ao tamanho excedido em relação ao limite para a 1ª geração..." da gravidade de error. Encontre o nome da função no campo functionName. Se o campo receiveTimestamp ainda estiver dentro de uma hora, será possível inferir o conteúdo real do evento lendo o documento em questão com um snapshot antes e depois do carimbo de data/hora.
    • Para evitar isso, faça o seguinte:
      • Migre e faça upgrade para o Cloud Functions (2ª geração)
      • Reduza o tamanho do documento
      • Exclua o Cloud Functions em questão
    • É possível desativar a geração de registros usando exclusões, mas os eventos ofensivos ainda não serão entregues.