Amplíe la base de datos en tiempo real con funciones en la nube


Con Cloud Functions, puedes manejar eventos en Firebase Realtime Database sin necesidad de actualizar el código del cliente. Cloud Functions le permite ejecutar operaciones de Realtime Database con privilegios administrativos completos y garantiza que cada cambio en Realtime Database se procese individualmente. Puede realizar cambios en la base de datos en tiempo real de Firebase a través de DataSnapshot o mediante Admin SDK .

En un ciclo de vida típico, una función de Firebase Realtime Database hace lo siguiente:

  1. Espera cambios en una ubicación particular de Realtime Database.
  2. Se activa cuando ocurre un evento y realiza sus tareas (consulte ¿Qué puedo hacer con Cloud Functions? para ver ejemplos de casos de uso).
  3. Recibe un objeto de datos que contiene una instantánea de los datos almacenados en el documento especificado.

Activar una función de base de datos en tiempo real

Cree nuevas funciones para eventos de Realtime Database con functions.database . Para controlar cuándo se activa la función, especifique uno de los controladores de eventos y especifique la ruta de la base de datos en tiempo real donde escuchará los eventos.

Establecer el controlador de eventos

Las funciones le permiten manejar eventos de bases de datos en tiempo real en dos niveles de especificidad; puede escuchar específicamente solo eventos de creación, actualización o eliminación, o puede escuchar cualquier cambio de cualquier tipo en una ruta. Cloud Functions admite estos controladores de eventos para Realtime Database:

  • onWrite() , que se activa cuando se crean, actualizan o eliminan datos en Realtime Database.
  • onCreate() , que se activa cuando se crean nuevos datos en Realtime Database.
  • onUpdate() , que se activa cuando se actualizan los datos en Realtime Database.
  • onDelete() , que se activa cuando se eliminan datos de Realtime Database.

Especificar la instancia y la ruta

Para controlar cuándo y dónde debe activarse su función, llame ref(path) para especificar una ruta y, opcionalmente, especifique una instancia de Realtime Database con instance('INSTANCE_NAME') . Si no especifica una instancia, la función se implementa en la instancia predeterminada de Realtime Database para el proyecto de Firebase. Por ejemplo:

  • Instancia predeterminada de base de datos en tiempo real: functions.database.ref('/foo/bar')
  • Instancia denominada "mi-aplicación-db-2": functions.database.instance('my-app-db-2').ref('/foo/bar')

Estos métodos dirigen su función para manejar escrituras en una ruta determinada dentro de la instancia de Realtime Database. Las especificaciones de ruta coinciden con todas las escrituras que tocan una ruta, incluidas las escrituras que ocurren en cualquier lugar debajo de ella. Si configura la ruta para su función como /foo/bar , coincidirá con eventos en ambas ubicaciones:

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

En cualquier caso, Firebase interpreta que el evento ocurre en /foo/bar y los datos del evento incluyen los datos antiguos y nuevos en /foo/bar . Si los datos del evento pueden ser grandes, considere usar múltiples funciones en rutas más profundas en lugar de una sola función cerca de la raíz de su base de datos. Para obtener el mejor rendimiento, solicite únicamente datos al nivel más profundo posible.

Puede especificar un componente de ruta como comodín rodeándolo entre llaves; ref('foo/{bar}') coincide con cualquier hijo de /foo . Los valores de estos componentes de ruta comodín están disponibles dentro del objeto EventContext.params de su función. En este ejemplo, el valor está disponible como context.params.bar .

Las rutas con comodines pueden coincidir con varios eventos de una sola escritura. un inserto de

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

coincide con la ruta "/foo/{bar}" dos veces: una con "hello": "world" y otra con "firebase": "functions" .

Manejar datos de eventos

Al manejar un evento de Realtime Database, el objeto de datos devuelto es un DataSnapshot . Para los eventos onWrite u onUpdate , el primer parámetro es un objeto Change que contiene dos instantáneas que representan el estado de los datos antes y después del evento desencadenante. Para los eventos onCreate y onDelete , el objeto de datos devuelto es una instantánea de los datos creados o eliminados.

En este ejemplo, la función recupera la instantánea de la ruta especificada, convierte la cadena en esa ubicación a mayúsculas y escribe esa cadena modificada en la base de datos:

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      functions.logger.log('Uppercasing', context.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 snapshot.ref.parent.child('uppercase').set(uppercase);
    });

Acceder a la información de autenticación del usuario

Desde EventContext.auth y EventContext.authType , puede acceder a la información del usuario, incluidos los permisos, para el usuario que activó una función. Esto puede resultar útil para hacer cumplir las reglas de seguridad, permitiendo que su función complete diferentes operaciones según el nivel de permisos del usuario:

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

exports.simpleDbFunction = functions.database.ref('/path')
    .onCreate((snap, context) => {
      if (context.authType === 'ADMIN') {
        // do something
      } else if (context.authType === 'USER') {
        console.log(snap.val(), 'written by', context.auth.uid);
      }
    });

Además, puede aprovechar la información de autenticación del usuario para "suplantar" a un usuario y realizar operaciones de escritura en su nombre. Asegúrese de eliminar la instancia de la aplicación como se muestra a continuación para evitar problemas de simultaneidad:

exports.impersonateMakeUpperCase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snap, context) => {
      const appOptions = JSON.parse(process.env.FIREBASE_CONFIG);
      appOptions.databaseAuthVariableOverride = context.auth;
      const app = admin.initializeApp(appOptions, 'app');
      const uppercase = snap.val().toUpperCase();
      const ref = snap.ref.parent.child('uppercase');

      const deleteApp = () => app.delete().catch(() => null);

      return app.database().ref(ref).set(uppercase).then(res => {
        // Deleting the app is necessary for preventing concurrency leaks
        return deleteApp().then(() => res);
      }).catch(err => {
        return deleteApp().then(() => Promise.reject(err));
      });
    });

Leyendo el valor anterior

El objeto Change tiene una propiedad before que le permite inspeccionar lo que se guardó en Realtime Database antes del evento. La propiedad before devuelve una DataSnapshot donde todos los métodos (por ejemplo, val() y exists() ) hacen referencia al valor anterior. Puede volver a leer el nuevo valor utilizando la DataSnapshot original o leyendo la propiedad after . Esta propiedad en cualquier Change es otra DataSnapshot que representa el estado de los datos después de que ocurrió el evento.

Por ejemplo, la propiedad before se puede usar para garantizar que la función solo escriba texto en mayúsculas cuando se crea por primera vez:

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onWrite((change, context) => {
      // Only edit data when it is first created.
      if (change.before.exists()) {
        return null;
      }
      // Exit when the data is deleted.
      if (!change.after.exists()) {
        return null;
      }
      // Grab the current value of what was written to the Realtime Database.
      const original = change.after.val();
      console.log('Uppercasing', context.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 change.after.ref.parent.child('uppercase').set(uppercase);
    });