Триггеры Cloud Firestore

С помощью Cloud Functions можно обрабатывать события в Cloud Firestore, не обновляя код клиента. Изменения в Cloud Firestore можно вносить через интерфейс снимка документа или с помощью Admin SDK.

В типичном жизненном цикле функция Cloud Firestore выполняет следующие действия:

  1. Ожидает изменений в определенном документе.
  2. Срабатывает при возникновении события и выполняет задачи.
  3. Получает объект данных, содержащий снимок данных, хранящихся в указанном документе. Для событий записи или обновления объект данных содержит два снимка, представляющих состояние данных до и после события.

Расстояние между местоположением экземпляра Firestore и местоположением функции может привести к значительной задержке сети. Чтобы повысить эффективность, при необходимости укажите местоположение функции.

Триггеры функций Cloud Firestore

Cloud Functions for Firebase SDK экспортирует следующие триггеры событий Cloud Firestore, позволяющие создавать обработчики, связанные с определенными событиями Cloud Firestore:

Node.js

Тип события Триггер
onDocumentCreated Срабатывает при первой записи в документ.
onDocumentUpdated Срабатывает, когда документ уже существует и в нем изменяется какое-либо значение.
onDocumentDeleted Срабатывает при удалении документа.
onDocumentWritten Срабатывает, когда срабатывает onDocumentCreated, onDocumentUpdated или onDocumentDeleted.
onDocumentCreatedWithAuthContext onDocumentCreated с дополнительной информацией для аутентификации
onDocumentWrittenWithAuthContext onDocumentWritten с дополнительной информацией для аутентификации
onDocumentDeletedWithAuthContext onDocumentDeleted с дополнительной информацией для аутентификации
onDocumentUpdatedWithAuthContext onDocumentUpdated с дополнительной информацией для аутентификации

Python

Тип события Триггер
on_document_created Срабатывает при первой записи в документ.
on_document_updated Срабатывает, когда документ уже существует и в нем изменяется какое-либо значение.
on_document_deleted Срабатывает при удалении документа.
on_document_written Срабатывает, когда срабатывает on_document_created, on_document_updated или on_document_deleted.
on_document_created_with_auth_context on_document_created с дополнительной информацией для аутентификации
on_document_updated_with_auth_context on_document_updated с дополнительной информацией для аутентификации
on_document_deleted_with_auth_context on_document_deleted с дополнительной информацией для аутентификации
on_document_written_with_auth_context on_document_written с дополнительной информацией для аутентификации

События Cloud Firestore запускаются только при изменении документов. Обновление документа Cloud Firestore, в котором данные не меняются (запись без изменений), не приводит к созданию события обновления или записи. Добавить события в определенные поля нельзя.

Если у вас ещё нет проекта, в котором включена функция Cloud Functions for Firebase, прочитайте статью Как начать работу с Cloud Functions for Firebase (второе поколение), чтобы настроить проект Cloud Functions for Firebase.

Как писать функции, запускаемые Cloud Firestore

Как задать триггер функции

Чтобы задать триггер Cloud Firestore, укажите путь к документу и тип события:

Node.js

const {
  onDocumentWritten,
  onDocumentCreated,
  onDocumentUpdated,
  onDocumentDeleted,
  Change,
  FirestoreEvent
} = require('firebase-functions/v2/firestore');

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

Python

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:

Пути к документам могут ссылаться на определенный документ или на шаблон с подстановочным знаком.

Как указать один документ

Если вы хотите, чтобы событие запускалось при любом изменении определенного документа, используйте следующую функцию:

Node.js

const {
  onDocumentWritten,
  Change,
  FirestoreEvent
} = require('firebase-functions/v2/firestore');

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

Python

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:

Как указать группу документов с помощью подстановочных знаков

Если вы хотите прикрепить триггер к группе документов, например к любому документу из определенной коллекции, используйте {wildcard} вместо идентификатора документа:

Node.js

const {
  onDocumentWritten,
  Change,
  FirestoreEvent
} = require('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

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

В этом примере при изменении любого поля в любом документе в папке users будет найдено соответствие с подстановочным знаком userId.

Если в документе в коллекции users есть вложенные коллекции и в одном из документов вложенной коллекции изменяется поле, подстановочный знак userId не активируется.

Соответствия с подстановочными знаками извлекаются из пути к документу и сохраняются в event.params. Вы можете определить любое количество подстановочных знаков для замены идентификаторов коллекций или документов, например:

Node.js

const {
  onDocumentWritten,
  Change,
  FirestoreEvent
} = require('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

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

Триггер всегда должен указывать на документ, даже если вы используете подстановочный знак. Например, users/{userId}/{messageCollectionId} недопустимо, поскольку {messageCollectionId} является коллекцией. Однако users/{userId}/{messageCollectionId}/{messageId} является допустимым вариантом, поскольку {messageId} всегда будет указывать на документ.

Триггеры событий

Как активировать функцию при создании нового документа

Вы можете настроить запуск функции при создании нового документа в коллекции. В этом примере функция запускается каждый раз, когда добавляется новый профиль пользователя:

Node.js

const {
  onDocumentCreated,
  Change,
  FirestoreEvent
} = require('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 ...
});

Для дополнительной аутентификации используйте onDocumentCreatedWithAuthContext.

Python

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

Как активировать функцию при обновлении документа

Также можно настроить запуск функции при обновлении документа. Приведенная ниже функция срабатывает, если пользователь меняет свой профиль:

Node.js

const {
  onDocumentUpdated,
  Change,
  FirestoreEvent
} = require('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 ...
});

Для дополнительной аутентификации используйте onDocumentUpdatedWithAuthContext.

Python

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

Как запустить функцию при удалении документа

Вы также можете настроить запуск функции при удалении документа. В приведенном ниже примере функция запускается, когда пользователь удаляет свой профиль:

Node.js

const {
  onDocumentDeleted,
  Change,
  FirestoreEvent
} = require('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 ...
});

Для дополнительной аутентификации используйте onDocumentDeletedWithAuthContext.

Python

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

Как активировать функцию при любых изменениях в документе

Если вам не важен тип события, можно отслеживать все изменения в документе Cloud Firestore с помощью триггера события "документ записан". Приведенная ниже функция срабатывает, если пользователь создан, обновлен или удален:

Node.js

const {
  onDocumentWritten,
  Change,
  FirestoreEvent
} = require('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 ...
});

Для дополнительной аутентификации используйте onDocumentWrittenWithAuthContext.

Python

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

Чтение и запись данных

Когда функция активируется, она предоставляет моментальный снимок данных, связанных с событием. Вы можете использовать этот снимок, чтобы читать данные из документа, вызвавшего событие, или записывать их в него. Также можно использовать Firebase Admin SDK для доступа к другим частям базы данных.

Данные о событиях

Данные о чтении

Когда функция активируется, вам может понадобиться получить данные из обновленного документа или данные до обновления. Чтобы получить предыдущие данные, используйте параметр event.data.before, который содержит снимок документа до обновления. Аналогично, event.data.after содержит состояние снимка документа после обновления.

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

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

Доступ к свойствам осуществляется так же, как и к свойствам любого другого объекта. Вы также можете использовать функцию get, чтобы получить доступ к определенным полям:

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

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

Запись данных

Каждый вызов функции связан с определенным документом в базе данных Cloud Firestore. Вы можете получить доступ к этому документу в снимке, возвращенном вашей функции.

Справочник по документам содержит такие методы, как update(), set() и remove(), которые позволяют изменять документ, вызвавший функцию.

Node.js

const {onDocumentUpdated} = require('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 event.data.after.ref.set({
    name_change_count: count + 1
  }, {merge: true});

});

Python

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

Как получить доступ к информации об аутентификации пользователей

Если вы используете один из следующих типов событий, то можете получить доступ к информации об аутентификации пользователя, который вызвал событие. Эти данные дополняют информацию, возвращаемую в базовом событии.

Node.js

  • onDocumentCreatedWithAuthContext
  • onDocumentWrittenWithAuthContext
  • onDocumentDeletedWithAuthContext
  • onDocumentUpdatedWithAuthContext

Python

  • on_document_created_with_auth_context
  • on_document_updated_with_auth_context
  • on_document_deleted_with_auth_context
  • on_document_written_with_auth_context

Информацию о данных, доступных в контексте аутентификации, можно найти в статье Контекст аутентификации. В примере ниже показано, как получить информацию об аутентификации:

Node.js

const {onDocumentWrittenWithAuthContext} = require('firebase-functions/v2/firestore');

exports.syncUser = onDocumentWrittenWithAuthContext("users/{userId}", (event) => {
    const snapshot = event.data.after;
    if (!snapshot) {
        console.log("No data associated with the event");
        return;
    }
    const data = snapshot.data();

    // retrieve auth context from event
    const { authType, authId } = event;

    let verified = false;
    if (authType === "system") {
      // system-generated users are automatically verified
      verified = true;
    } else if (authType === "unknown" || authType === "unauthenticated") {
      // admin users from a specific domain are verified
      if (authId.endsWith("@example.com")) {
        verified = true;
      }
    }

    return data.after.ref.set({
        created_by: authId,
        verified,
    }, {merge: true}); 
}); 

Python

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

  # Get the auth context from the event
  user_auth_type = event.auth_type
  user_auth_id = event.auth_id

Данные за пределами события-триггера

Cloud Functions выполняются в доверенной среде. Они авторизованы как сервисный аккаунт в вашем проекте, и вы можете выполнять операции чтения и записи с помощью Firebase Admin SDK:

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

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

Ограничения

Обратите внимание на следующие ограничения для триггеров Cloud Firestore для облачных функций:

  • Для Cloud Functions (первого поколения) требуется существующая база данных "(default)" в стандартном режиме Firestore. Он не поддерживает базы данных с именами Cloud Firestore и режим хранилища данных. В таких случаях для настройки событий используйте Cloud Functions (второе поколение).
  • Настройка между проектами с триггерами Cloud Functions и Cloud Firestore не поддерживается. Чтобы настроить триггер Cloud Firestore, триггер Cloud Functions должен находиться в том же проекте.
  • Порядок не гарантируется. Быстрые изменения могут привести к тому, что функции будут вызываться в неожиданном порядке.
  • События доставляются хотя бы один раз, но одно событие может привести к нескольким вызовам функции. Не полагайтесь на механизмы, обеспечивающие доставку ровно один раз, и пишите идемпотентные функции.
  • Cloud Firestore в режиме хранилища данных требует Cloud Functions (второе поколение). Cloud Functions первого поколения не поддерживает режим Datastore.
  • Триггер связан с одной базой данных. Нельзя создать триггер, который соответствует нескольким базам данных.
  • При удалении базы данных триггеры для нее не удаляются автоматически. Триггер перестанет активировать события, но будет существовать, пока вы не удалите его.
  • Если размер подходящего события превышает максимальный размер запроса, событие может не быть доставлено в Cloud Functions (первое поколение).
    • События, не доставленные из-за размера запроса, регистрируются в журналах платформы и учитываются при подсчете использования журналов для проекта.
    • Эти журналы можно найти в проводнике журналов с сообщением "Event cannot deliver to Cloud function due to size exceeding the limit for 1st gen..." errorуровня серьезности. Название функции можно найти в поле functionName. Если значение поля receiveTimestamp находится в пределах часа от текущего времени, вы можете определить фактическое содержимое события, прочитав документ, о котором идет речь, с помощью моментального снимка до и после временной метки.
    • Чтобы избежать этого, вы можете:
      • Переход на Nest Protect Cloud Functions 2-го поколения
      • Как уменьшить размер документа
      • Удалите Cloud Functions.
    • Вы можете отключить ведение журнала с помощью исключений, но учтите, что события, нарушающие правила, по-прежнему не будут доставляться.