Extender Firestore con Cloud Functions

Con Cloud Functions, puedes desplegar código de Node.js para gestionar eventos activados por cambios en tu base de datos de Firestore. De esta forma, puedes añadir fácilmente funciones del lado del servidor a tu aplicación sin tener que ejecutar tus propios servidores.

Para ver ejemplos de casos prácticos, consulta ¿Qué puedo hacer con Cloud Functions? o el repositorio de GitHub Functions Samples.

Activadores de funciones de Firestore

El SDK de Cloud Functions para Firebase exporta un objeto functions.firestore que te permite crear controladores vinculados a eventos específicos de Firestore.

Tipo de evento Activador
onCreate Se activa cuando se escribe en un documento por primera vez.
onUpdate Se activa cuando ya existe un documento y se cambia algún valor.
onDelete Se activa cuando se elimina un documento con datos.
onWrite Se activa cuando se activa onCreate, onUpdate o onDelete.

Si aún no tienes ningún proyecto habilitado para Cloud Functions for Firebase, consulta el artículo Primeros pasos: escribe e implementa tus primeras funciones para configurar tu proyecto de Cloud Functions for Firebase.

Escribir funciones activadas por Firestore

Definir un activador de función

Para definir un activador de Firestore, especifica una ruta de documento y un tipo de evento:

Node.js

const functions = require('firebase-functions');

exports.myFunction = functions.firestore
  .document('my-collection/{docId}')
  .onWrite((change, context) => { /* ... */ });

Las rutas de documentos pueden hacer referencia a un documento específico o a un patrón comodín.

Especificar un solo documento

Si quieres activar un evento para cualquier cambio en un documento específico, puedes usar la siguiente función.

Node.js

// Listen for any change on document `marie` in collection `users`
exports.myFunctionName = functions.firestore
    .document('users/marie').onWrite((change, context) => {
      // ... Your code here
    });

Especificar un grupo de documentos mediante comodines

Si quieres adjuntar un activador a un grupo de documentos, como cualquier documento de una colección determinada, usa un {wildcard} en lugar del ID del documento:

Node.js

// Listen for changes in all documents in the 'users' collection
exports.useWildcard = functions.firestore
    .document('users/{userId}')
    .onWrite((change, context) => {
      // If we set `/users/marie` to {name: "Marie"} then
      // context.params.userId == "marie"
      // ... and ...
      // change.after.data() == {name: "Marie"}
    });

En este ejemplo, cuando se cambia cualquier campo de cualquier documento de users, coincide con un comodín llamado userId.

Si un documento de users tiene subcolecciones y se cambia un campo de uno de los documentos de esas subcolecciones, no se activa el comodín userId.

Las coincidencias con comodines se extraen de la ruta del documento y se almacenan en context.params. Puedes definir tantos comodines como quieras para sustituir los IDs de colecciones o documentos explícitos. Por ejemplo:

Node.js

// Listen for changes in all documents in the 'users' collection and all subcollections
exports.useMultipleWildcards = functions.firestore
    .document('users/{userId}/{messageCollectionId}/{messageId}')
    .onWrite((change, context) => {
      // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
      // context.params.userId == "marie";
      // context.params.messageCollectionId == "incoming_messages";
      // context.params.messageId == "134";
      // ... and ...
      // change.after.data() == {body: "Hello"}
    });

Activadores de eventos

Activar una función cuando se cree un documento

Puedes activar una función para que se ejecute cada vez que se cree un documento en una colección mediante un controlador onCreate() con un comodín. Esta función de ejemplo llama a createUser cada vez que se añade un nuevo perfil de usuario:

Node.js

exports.createUser = functions.firestore
    .document('users/{userId}')
    .onCreate((snap, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = snap.data();

      // access a particular field as you would any JS property
      const name = newValue.name;

      // perform desired operations ...
    });

Activar una función cuando se actualiza un documento

También puedes activar una función para que se ejecute cuando se actualice un documento mediante la función onUpdate() con un comodín. Esta función de ejemplo llama a updateUser si un usuario cambia su perfil:

Node.js

exports.updateUser = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();

      // access a particular field as you would any JS property
      const name = newValue.name;

      // perform desired operations ...
    });

Activar una función cuando se elimine un documento

También puedes activar una función cuando se elimine un documento usando la función onDelete() con un comodín. En este ejemplo, la función llama a deleteUser cuando un usuario elimina su perfil:

Node.js

exports.deleteUser = functions.firestore
    .document('users/{userID}')
    .onDelete((snap, context) => {
      // Get an object representing the document prior to deletion
      // e.g. {'name': 'Marie', 'age': 66}
      const deletedValue = snap.data();

      // perform desired operations ...
    });

Activar una función para todos los cambios de un documento

Si no te importa el tipo de evento que se activa, puedes monitorizar todos los cambios de un documento de Firestore mediante la función onWrite() con un comodín. Esta función de ejemplo llama a modifyUser si se crea, actualiza o elimina un usuario:

Node.js

exports.modifyUser = functions.firestore
    .document('users/{userID}')
    .onWrite((change, context) => {
      // Get an object with the current document value.
      // If the document does not exist, it has been deleted.
      const document = change.after.exists ? change.after.data() : null;

      // Get an object with the previous document value (for update or delete)
      const oldDocument = change.before.data();

      // perform desired operations ...
    });

Leer y escribir datos

Cuando se activa una función, se proporciona una instantánea de los datos relacionados con el evento. Puedes usar esta instantánea para leer o escribir en el documento que ha activado el evento, o bien usar el SDK de administrador de Firebase para acceder a otras partes de tu base de datos.

Datos de eventos

Leer datos

Cuando se activa una función, es posible que quieras obtener datos de un documento que se haya actualizado o los datos anteriores a la actualización. Puedes obtener los datos anteriores mediante change.before.data(), que contiene la captura del documento antes de la actualización. Del mismo modo, change.after.data() contiene el estado de la captura del documento después de la actualización.

Node.js

exports.updateUser2 = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the current document
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();
    });

Puede acceder a las propiedades como lo haría en cualquier otro objeto. También puedes usar la función get para acceder a campos específicos:

Node.js

// Fetch data using standard accessors
const age = snap.data().age;
const name = snap.data()['name'];

// Fetch data using built in accessor
const experience = snap.get('experience');

Escribir datos

Cada invocación de función se asocia a un documento específico de tu base de datos de Firestore. Puedes acceder a ese documento como DocumentReference en la propiedad ref de la vista del día devuelta a tu función.

Este DocumentReference procede del SDK de Node.js de Firestore e incluye métodos como update(), set() y remove() para que puedas modificar fácilmente el documento que ha activado la función.

Node.js

// Listen for updates to any `user` document.
exports.countNameChanges = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Retrieve the current and previous value
      const data = change.after.data();
      const previousData = change.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 change.after.ref.set({
        name_change_count: count + 1
      }, {merge: true});
    });

Datos fuera del evento de activación

Cloud Functions se ejecuta en un entorno de confianza, lo que significa que se autoriza como una cuenta de servicio en tu proyecto. Puedes realizar lecturas y escrituras con el SDK de administrador de Firebase:

Node.js

const admin = require('firebase-admin');
admin.initializeApp();

const db = admin.firestore();

exports.writeToFirestore = functions.firestore
  .document('some/doc')
  .onWrite((change, context) => {
    db.doc('some/otherdoc').set({ ... });
  });

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 campo functionName. Si el campo receiveTimestamp 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.