Activadores de Google Cloud Firestore (1.ª gen.)
Las funciones de Cloud Run pueden gestionar eventos en Firestore en el mismo Google Cloud proyecto que la función. Puedes leer o actualizar Firestore en respuesta a estos eventos mediante las APIs y bibliotecas de cliente de Firestore.
En un ciclo de vida típico, una función de Firestore hace lo siguiente:
Espera a que se produzcan cambios en un documento concreto.
Se activa cuando se produce un evento y realiza sus tareas.
Recibe un objeto de datos con una instantánea del documento afectado. En el caso de los eventos
write
oupdate
, el objeto de datos contiene capturas que representan el estado del documento antes y después del evento activador.
Tipos de eventos
Firestore admite eventos create
, update
, delete
y write
. El evento write
abarca todas las modificaciones que se hagan en un documento.
Tipo de evento | Activador |
---|---|
providers/cloud.firestore/eventTypes/document.create (predeterminado) |
Se activa cuando se escribe en un documento por primera vez. |
providers/cloud.firestore/eventTypes/document.update |
Se activa cuando ya existe un documento y se cambia algún valor. |
providers/cloud.firestore/eventTypes/document.delete |
Se activa cuando se elimina un documento con datos. |
providers/cloud.firestore/eventTypes/document.write |
Se activa cuando se crea, actualiza o elimina un documento. |
Los comodines se escriben en los activadores con llaves, de la siguiente manera:
"projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}"
Especificar la ruta del documento
Para activar la función, especifica una ruta de documento que se va a monitorizar. Las funciones solo responden a los cambios en los documentos y no pueden monitorizar campos ni colecciones específicos. A continuación se muestran algunos ejemplos de rutas de documentos válidas:
users/marie
: activador válido. Monitoriza un solo documento,/users/marie
.users/{username}
: activador válido. Monitoriza todos los documentos de los usuarios. Los comodines se usan para monitorizar todos los documentos de la colección.users/{username}/addresses
: activador no válido. Hace referencia a la subcolecciónaddresses
, no a un documento.users/{username}/addresses/home
: activador válido. Monitoriza el documento de domicilio de todos los usuarios.users/{username}/addresses/{addressId}
: activador válido. Monitoriza todos los documentos de dirección.
Usar comodines y parámetros
Si no sabes qué documento quieres monitorizar, usa {wildcard}
en lugar del ID del documento:
users/{username}
escucha los cambios en todos los documentos de los usuarios.
En este ejemplo, cuando se cambia cualquier campo de cualquier documento de users
, coincide con un comodín llamado {username}
.
Si un documento de users
tiene subcolecciones y se cambia un campo de uno de los documentos de esas subcolecciones, el comodín {username}
no se activará.
Las coincidencias con comodines se extraen de las rutas de los documentos. Puedes definir tantos comodines como quieras para sustituir los IDs de colecciones o documentos explícitos.
Estructura de eventos
Este activador invoca tu función con un evento similar al que se muestra a continuación:
{ "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
contiene uno o varios objetos Value
. Consulta la documentación de Value
para obtener referencias de tipos. Esto es especialmente útil si usas un lenguaje tipado (como Go) para escribir tus funciones.
Código de ejemplo
La función de Cloud Functions de ejemplo que aparece a continuación imprime los campos de un evento de Cloud Firestore activador:
Node.js
Python
Go
Java
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)); } }
Ruby
PHP
use Google\CloudFunctions\CloudEvent; function firebaseFirestore(CloudEvent $cloudevent) { $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); fwrite($log, 'Event: ' . $cloudevent->getId() . PHP_EOL); fwrite($log, 'Event Type: ' . $cloudevent->getType() . PHP_EOL); $data = $cloudevent->getData(); $resource = $data['resource']; fwrite($log, 'Function triggered by event on: ' . $resource . PHP_EOL); if (isset($data['oldValue'])) { fwrite($log, 'Old value: ' . json_encode($data['oldValue']) . PHP_EOL); } if (isset($data['value'])) { fwrite($log, 'New value: ' . json_encode($data['value']) . PHP_EOL); } }
En el ejemplo siguiente se obtiene el valor añadido por el usuario, se convierte la cadena de esa ubicación a mayúsculas y se sustituye el valor por la cadena en mayúsculas:
Node.js
Python
Go
Java
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); } }
Ruby
PHP
use Google\Cloud\Firestore\FirestoreClient; use Google\CloudFunctions\CloudEvent; function firebaseReactive(CloudEvent $cloudevent) { $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); $data = $cloudevent->getData(); $resource = $data['value']['name']; $db = new FirestoreClient(); $docPath = explode('/documents/', $resource)[1]; $affectedDoc = $db->document($docPath); $curValue = $data['value']['fields']['original']['stringValue']; $newValue = strtoupper($curValue); if ($curValue !== $newValue) { fwrite($log, 'Replacing value: ' . $curValue . ' --> ' . $newValue . PHP_EOL); $affectedDoc->set(['original' => $newValue]); } else { // Value is already upper-case // Don't perform another write (it might cause an infinite loop) fwrite($log, 'Value is already upper-case.' . PHP_EOL); } }
Desplegar una función
El siguiente comando gcloud
implementa una función que se activa
cuando se escriben datos en el documento /messages/{pushId}
:
gcloud functions deploy FUNCTION_NAME \ --no-gen2 \ --entry-point ENTRY_POINT \ --runtime RUNTIME \ --set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID \ --trigger-event "providers/cloud.firestore/eventTypes/document.write" \ --trigger-resource "projects/YOUR_PROJECT_ID/databases/(default)/documents/messages/{pushId}"
Argumento | Descripción |
---|---|
FUNCTION_NAME |
Nombre registrado de la función de Cloud Functions que vas a desplegar.
Puede ser el nombre de una función de tu código fuente o una cadena arbitraria. Si FUNCTION_NAME es una cadena arbitraria, debes incluir la marca --entry-point .
|
--entry-point ENTRY_POINT |
El nombre de una función o una clase en el código fuente. Opcional, a menos que no hayas usado FUNCTION_NAME para especificar la función en el código fuente que se va a ejecutar durante la implementación. En ese caso, debes usar --entry-point para indicar el nombre de la función ejecutable.
|
--runtime RUNTIME |
El nombre del tiempo de ejecución que estás usando. Para ver una lista completa, consulta la referencia de gcloud .
|
--set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID |
El identificador único del proyecto como variable de entorno de tiempo de ejecución. |
--trigger-event NAME |
El tipo de evento que monitorizará la función (uno de los siguientes: write , create , update o delete ).
|
--trigger-resource NAME |
Ruta de la base de datos completa a la que se conectará la función.
Debe tener el siguiente formato:
"projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH"
El texto {pushId} es un parámetro comodín que se describe más arriba
en Especificar la ruta del documento.
|
Limitaciones
Ten en cuenta las siguientes limitaciones de los activadores de Firestore para las funciones de Cloud Run:
- Las funciones de Cloud Run (1.ª gen.) requieren una base de datos "(default)" en el modo nativo de Firestore. No admite bases de datos con nombre de Firestore ni el modo Datastore. En estos casos, utiliza funciones de Cloud Run (2.ª gen.) para configurar los eventos.
- No se garantiza la realización del pedido. Los cambios rápidos pueden activar invocaciones de funciones en un orden inesperado.
- Los eventos se entregan al menos una vez, pero un solo evento puede dar lugar a varias invocaciones de funciones. No dependas de los mecanismos de entrega exactamente una vez y escribe funciones idempotentes.
- Firestore en el modo de Datastore requiere funciones de Cloud Run (2.ª gen.). Cloud Run Functions (1.ª gen.) no admite el modo Datastore.
- Un activador está asociado a una sola base de datos. No puedes crear un activador que coincida con varias bases de datos.
- Si eliminas una base de datos, no se eliminarán automáticamente los activadores de esa base de datos. El activador deja de enviar eventos, pero sigue existiendo hasta que lo eliminas.
- Si un evento coincidente supera el tamaño máximo de solicitud, es posible que no se envíe a las funciones de Cloud Run (1.ª gen.).
- Los eventos que no se entregan debido al tamaño de la solicitud se registran en los registros de la plataforma y se tienen en cuenta en el uso de registros del proyecto.
- Puedes encontrar estos registros en el Explorador de registros con el mensaje "Event cannot deliver to
Cloud function due to size exceeding the limit for 1st gen..." (No se puede enviar el evento a la función de Cloud porque el tamaño supera el límite de la primera generación...) de
error
gravedad. Puedes encontrar el nombre de la función en el campofunctionName
. Si el camporeceiveTimestamp
sigue estando a menos de una hora, puedes inferir el contenido del evento leyendo el documento en cuestión con una instantánea antes y después de la marca de tiempo. - Para evitar este tipo de cadencia, puedes hacer lo siguiente:
- Migrar y actualizar a Cloud Run Functions (2.ª gen.)
- Reducir el tamaño del documento
- Elimina las funciones de Cloud Run en cuestión.
- Puedes desactivar el registro mediante exclusiones, pero ten en cuenta que los eventos infractores seguirán sin enviarse.