Триггеры Realtime Database

С помощью Cloud Functions можно обрабатывать события в Firebase Realtime Database без необходимости обновлять код клиента. Cloud Functions позволяет выполнять операции Realtime Database с полными правами администратора и гарантирует, что каждое изменение в Realtime Database будет обработано отдельно. Вы можете внести Firebase Realtime Database изменения с помощью снимка данных или Admin SDK.

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

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

Вы можете активировать функцию в ответ на запись, создание, обновление или удаление узлов базы данных в Firebase Realtime Database. Чтобы указать, когда должна запускаться функция, задайте один из обработчиков событий и путь к Realtime Database, где будут отслеживаться события.

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

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

Как обрабатывать события Realtime Database

Функции позволяют обрабатывать события Realtime Database на двух уровнях детализации: можно отслеживать только события записи, создания, обновления или удаления или любые изменения в ссылке.

Доступны следующие обработчики для реагирования на события Realtime Database:

Node.js

  • onValueWritten() Срабатывает, когда данные создаются, обновляются или удаляются в Realtime Database.
  • onValueCreated() Срабатывает только при создании данных в Realtime Database.
  • onValueUpdated() Запускается только при обновлении данных в Realtime Database.
  • onValueDeleted() Срабатывает только при удалении данных в Realtime Database.

Python

  • on_value_written() Срабатывает, когда данные создаются, обновляются или удаляются в Realtime Database.
  • on_value_created() Срабатывает только при создании данных в Realtime Database.
  • on_value_updated() Запускается только при обновлении данных в Realtime Database.
  • on_value_deleted() Срабатывает только при удалении данных в Realtime Database.

Как импортировать необходимые модули

В исходном коде функции необходимо импортировать модули SDK, которые вы хотите использовать. В этом примере необходимо импортировать модули HTTP и Realtime Database, а также модуль Firebase Admin SDK для записи в Realtime Database.

Node.js

// The Cloud Functions for Firebase SDK to setup triggers and logging.
const {onRequest} = require("firebase-functions/https");
const {onValueCreated} = require("firebase-functions/database");
const {logger} = require("firebase-functions");

// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require("firebase-admin");
admin.initializeApp();

Python

# The Cloud Functions for Firebase SDK to create Cloud Functions and set up triggers.
from firebase_functions import db_fn, https_fn

# The Firebase Admin SDK to access the Firebase Realtime Database.
from firebase_admin import initialize_app, db

app = initialize_app()

Как указать экземпляр и путь

Чтобы указать, когда и где должна запускаться функция, настройте ее с помощью пути и, при необходимости, экземпляра Realtime Database. Если вы не укажете экземпляр, функция будет прослушивать все экземпляры Realtime Database в регионе функции. Вы также можете указать шаблон экземпляра Realtime Database, чтобы развернуть его на выбранных экземплярах в том же регионе.

Пример:

Node.js

// All Realtime Database instances at path "/user/{uid}"
// There must be at least one Realtime Database present.
const onWrittenFunctionDefault = onValueWritten("/user/{uid}", (event) => {
  // …
});

// Instance named "my-app-db-2", at path "/user/{uid}".
// The "my-app-db-2" instance must exist in this region.
const OnWrittenFunctionInstance = onValueWritten(
  {
    ref: "/user/{uid}",
    instance: "my-app-db-2"
  },
  (event) => {
    // …
  }
);

// Instance with "my-app-db-" prefix, at path "/user/{uid}", where uid ends with @gmail.com.
// There must be at least one Realtime Database with "my-app-db-*" prefix in this region.
const onWrittenFunctionInstance = onValueWritten(
  {
    ref: "/user/{uid=*@gmail.com}",
    instance: "my-app-db-*"
  },
  (event) => {
    // …
  }
);

Python

# All Realtime Database instances at path "/user/{uid}"
# There must be at least one Realtime Database present.
@db_fn.on_value_written(r"/user/{uid}")
def onwrittenfunctiondefault(event: db_fn.Event[db_fn.Change]):
    # ...
    pass

# Instance named "my-app-db-2", at path "/user/{uid}".
# The "my-app-db-2" instance must exist in this region.
@db_fn.on_value_written(
    reference=r"/user/{uid}",
    instance="my-app-db-2",
)
def on_written_function_instance(event: db_fn.Event[db_fn.Change]):
    # ...
    pass

# Instance with "my-app-db-" prefix, at path "/user/{uid}", where uid ends with @gmail.com.
# There must be at least one Realtime Database with "my-app-db-*" prefix in this region.
@db_fn.on_value_written(
    reference=r"/user/{uid=*@gmail.com}",
    instance="my-app-db-*",
)
def on_written_function_instance(event: db_fn.Event[db_fn.Change]):
    # ...
    pass

Эти параметры указывают функции, как обрабатывать записи по определенному пути в экземпляре Realtime Database.

Спецификации пути соответствуют всем операциям записи, которые затрагивают путь, в том числе операциям записи, которые происходят в любом месте ниже него. Если вы зададите для функции путь /foo/bar, она будет сопоставлять события в обоих следующих местоположениях:

 /foo/bar
 /foo/bar/baz/really/deep/path

В обоих случаях Firebase интерпретирует событие как произошедшее в /foo/bar, а данные о событии включают старые и новые данные в /foo/bar. Если данные о событиях могут быть большими, вместо одной функции в корне базы данных используйте несколько функций на более глубоких уровнях. Чтобы повысить эффективность, запрашивайте данные только на самом глубоком уровне.

Уайлдкард и захват

Для записи можно использовать {key}, {key=*}, {key=prefix*}, {key=*suffix}. *, prefix*, *suffix для подстановки одного сегмента. Примечание. ** – это многосегментный подстановочный знак, который не поддерживается в Realtime Database. Подробнее о шаблонах путей…

Подстановка символов в пути. Вы можете указать компонент пути как подстановочный знак:

  • Используйте звездочку, *. Например, foo/* соответствует всем дочерним элементам на один уровень ниже foo/.
  • Используя сегмент, содержащий только звездочку, *. Например, foo/app*-us соответствует любому дочернему сегменту ниже foo/ с префиксом app и суффиксом -us.

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

{
  "foo": {
    "hello": "world",
    "firebase": "functions"
  }
}

соответствует пути "/foo/*" дважды: один раз с "hello": "world" и ещё раз с "firebase": "functions".

Запись пути. Вы можете сохранять совпадения путей в именованные переменные, чтобы использовать их в коде функции (например, /user/{uid}, /user/{uid=*-us}).

Значения переменных, полученные с помощью функции захвата, доступны в объекте database.DatabaseEvent.params.

Подстановка символов в названиях экземпляров. Также можно указать компонент экземпляра с помощью подстановочных знаков. Подстановочный знак экземпляра может иметь префикс, суффикс или и то и другое (например, my-app-*-prod).

Справочная информация о подстановочных знаках и захвате

В Cloud Functions (второго поколения) и Realtime Database при указании ref и instance можно использовать шаблон. В каждом интерфейсе триггера будут доступны следующие варианты области действия функции:

Указание ref Указание instance Алгоритм работы
Сингл (/foo/bar) Не указывать Область действия обработчика распространяется на все экземпляры в регионе функции.
Сингл (/foo/bar) Сингл (‘my-new-db') Обработчик областей для определенного экземпляра в регионе функции.
Сингл (/foo/bar) Узор (‘inst-prefix*') Обработчик областей для всех экземпляров, соответствующих шаблону в регионе функции.
Узор (/foo/{bar}) Не указывать Область действия обработчика распространяется на все экземпляры в регионе функции.
Узор (/foo/{bar}) Сингл (‘my-new-db') Обработчик областей для определенного экземпляра в регионе функции.
Узор (/foo/{bar}) Узор (‘inst-prefix*') Обработчик областей для всех экземпляров, соответствующих шаблону в регионе функции.

Как обрабатывать данные о событиях

Когда срабатывает событие Realtime Database, оно передает объект Event функции обработчика. У этого объекта есть свойство data, которое для событий создания и удаления содержит снимок созданных или удаленных данных.

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

Node.js

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
// for all databases in 'us-central1'
exports.makeuppercase = onValueCreated(
    "/messages/{pushId}/original",
    (event) => {
    // Grab the current value of what was written to the Realtime Database.
      const original = event.data.val();
      logger.log("Uppercasing", event.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing
      // asynchronous tasks inside a function, such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the
      // Realtime Database returns a Promise.
      return event.data.ref.parent.child("uppercase").set(uppercase);
    },
);

Python

@db_fn.on_value_created(reference="/messages/{pushId}/original")
def makeuppercase(event: db_fn.Event[Any]) -> None:
    """Listens for new messages added to /messages/{pushId}/original and
    creates an uppercase version of the message to /messages/{pushId}/uppercase
    """

    # Grab the value that was written to the Realtime Database.
    original = event.data
    if not isinstance(original, str):
        print(f"Not a string: {event.reference}")
        return

    # Use the Admin SDK to set an "uppercase" sibling.
    print(f"Uppercasing {event.params['pushId']}: {original}")
    upper = original.upper()
    parent = db.reference(event.reference).parent
    if parent is None:
        print("Message can't be root node.")
        return
    parent.child("uppercase").set(upper)

Прочитать предыдущее значение

Для событий write или update свойство data представляет собой объект Change, содержащий два снимка, которые отражают состояние данных до и после запускающего события. У объекта Change есть свойство before, которое позволяет проверить, что было сохранено в Realtime Database до события, и свойство after, которое представляет состояние данных после события.

Например, свойство before можно использовать, чтобы функция переводила текст в верхний регистр только при его создании:

Node.js

  exports makeUppercase = onValueWritten("/messages/{pushId}/original", (event) => {
        // Only edit data when it is first created.
        if (event.data.before.exists()) {
          return null;
        }
        // Exit when the data is deleted.
        if (!event.data.after.exists()) {
          return null;
        }
        // Grab the current value of what was written to the Realtime Database.
        const original = event.data.after.val();
        console.log('Uppercasing', event.params.pushId, original);
        const uppercase = original.toUpperCase();
        // You must return a Promise when performing asynchronous tasks inside a Functions such as
        // writing to the Firebase Realtime Database.
        // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
        return event.data.after.ref.parent.child('uppercase').set(uppercase);
      });

Python

@db_fn.on_value_written(reference="/messages/{pushId}/original")
def makeuppercase2(event: db_fn.Event[db_fn.Change]) -> None:
    """Listens for new messages added to /messages/{pushId}/original and
    creates an uppercase version of the message to /messages/{pushId}/uppercase
    """

    # Only edit data when it is first created.
    if event.data.before is not None:
        return

    # Exit when the data is deleted.
    if event.data.after is None:
        return

    # Grab the value that was written to the Realtime Database.
    original = event.data.after
    if not hasattr(original, "upper"):
        print(f"Not a string: {event.reference}")
        return

    # Use the Admin SDK to set an "uppercase" sibling.
    print(f"Uppercasing {event.params['pushId']}: {original}")
    upper = original.upper()
    parent = db.reference(event.reference).parent
    if parent is None:
        print("Message can't be root node.")
        return
    parent.child("uppercase").set(upper)

Как получить доступ к контексту аутентификации

Для функций, запускаемых событиями Eventarc RTDB, контекст аутентификации включается в полезную нагрузку события:

  • authtype – тип субъекта, который запустил событие. Возможные значения:
    • app_user: конечный пользователь приложения разработчика.
    • admin – сервисный аккаунт.
    • unauthenticated – неаутентифицированный пользователь.
    • unknown – значение по умолчанию, если информация для аутентификации недоступна.
  • authid – уникальный идентификатор субъекта.
    • Если параметр authtype имеет значение app_user, это уникальный идентификатор пользователя.
    • Если authtype – это admin, то это адрес электронной почты сервисного аккаунта или пользователя IAM.

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

Node.js

// The Cloud Functions for Firebase SDK to setup triggers and logging.
const {onValueWritten} = require("firebase-functions/v2/database");
const {logger} = require("firebase-functions");
const admin = require("firebase-admin");

admin.initializeApp();

exports.dbtrigger = onValueWritten("/messages/{pushId}/original", async (event) => {
  // 1. Check whether authtype is admin. If it is, skip this operation.
  if (event.authType === "admin") {
    logger.log("Modification by admin detected. Skipping uppercase conversion.");
    return null;
  }

  // 2. Retrieve the userID of the sender (assumed sibling node 'senderId')
  const snapshot = await event.data.after.ref.parent.child("senderId").get();
  const senderId = snapshot.val();

  // 3. Check if userID of sender of message = event.authid
  if (senderId !== event.authId) {
    logger.error(`Unauthorized write: senderId (${senderId}) does not match authId (${event.authId})`);
    return null;
  }

  // Grab the value that was written to the Realtime Database.
  const original = event.data.after.val();
  logger.log("Uppercasing", event.params.pushId, original);
  const uppercase = original.toUpperCase();

  // Return the promise to set the "uppercase" sibling node.
  return event.data.after.ref.parent.child("uppercase").set(uppercase);
});

Python

from firebase_functions import db_fn
from firebase_admin import initialize_app, db

initialize_app()

@db_fn.on_value_written(reference="/messages/{pushId}/original")
def makeuppercase(event: db_fn.Event[db_fn.Change]) -> None:
    # 1. Check whether authtype is admin. If it is, skip this operation.
    if event.auth_type == "admin":
        print("Admin user detected. Skipping.")
        return

    # 2. Retrieve the userID of the sender (assumed sibling node: 'senderId')
    parent_ref = db.reference(event.reference).parent
    sender_id = parent_ref.child("senderId").get()

    # 3. Check if userID of sender = event.auth_id
    if sender_id != event.auth_id:
        print(f"Unauthorized: sender_id {sender_id} != auth_id {event.auth_id}")
        return

    # Exit when the data is deleted.
    if event.data.after is None:
        return

    # Grab the value and uppercase it
    original = event.data.after
    if not isinstance(original, str):
        return

    print(f"Uppercasing {event.params['pushId']}: {original}")
    upper = original.upper()
    parent_ref.child("uppercase").set(upper)