Amplía Realtime Database con Cloud Functions


Con Cloud Functions, puedes administrar eventos en Firebase Realtime Database sin necesidad de actualizar el código del cliente. Cloud Functions te permite ejecutar operaciones de Realtime Database con privilegios de administrador completos y garantiza que cada cambio en Realtime Database se procese de forma individual. Puedes hacer cambios en Firebase Realtime Database mediante DataSnapshot o el SDK de Admin.

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

  1. Espera cambios en una ubicación de Realtime Database específica.
  2. Se activa cuando ocurre un evento y realiza sus tareas (consulta ¿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.

Activa una función de Realtime Database

Crea nuevas funciones para los eventos de Realtime Database con functions.database. Para controlar cuándo se debe activar la función, especifica uno de los controladores de eventos y la ruta de acceso de Realtime Database en la que se detectarán los eventos.

Configura el controlador de eventos

Las funciones te permiten controlar los eventos de Realtime Database en dos niveles de especificidad: puedes detectar específicamente solo eventos de creación, actualización o eliminación, o puedes detectar cambios de cualquier tipo en una ruta de acceso. Cloud Functions es compatible con los siguientes controladores de eventos para Realtime Database:

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

Especifica la instancia y la ruta

Para controlar el momento y la ubicación en los que se debe activar la función, llama a ref(path) a fin de especificar una ruta de acceso y, opcionalmente, utiliza instance('INSTANCE_NAME') para especificar una instancia de Realtime Database. Si no especificas una instancia, la función se implementa en la instancia de Realtime Database predeterminada del proyecto de Firebase. Por ejemplo:

  • Instancia predeterminada de Realtime Database: functions.database.ref('/foo/bar')
  • Instancia denominada “my-app-db-2”: functions.database.instance('my-app-db-2').ref('/foo/bar')

Estos métodos le indican a la función que realice las operaciones de escritura en una ruta de acceso específica dentro de la instancia de Realtime Database. Las coincidencias de las especificaciones de la ruta de acceso se establecen con todas las escrituras que afectan una ruta, incluidas las que ocurren por debajo de esa ruta. Si configuras la ruta para la función como /foo/bar, se establecerán coincidencias con los eventos que ocurren en estas dos ubicaciones:

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

En ambos casos, Firebase interpreta que el evento ocurre en /foo/bar, y los datos del evento incluyen los datos antiguos y nuevos en /foo/bar. Si existe la posibilidad de que los eventos generen un gran volumen de datos, tal vez sea conveniente que uses varias funciones en rutas de acceso más profundas en lugar de una sola función cerca de la raíz de la base de datos. Para obtener el mejor rendimiento, solicita datos únicamente en el nivel más profundo posible.

Para especificar un componente de ruta de acceso como comodín, puedes ponerlo entre llaves. Por ejemplo, se determinará la existencia de coincidencia entre ref('foo/{bar}') y cualquier elemento secundario de /foo. Los valores de estos componentes de ruta de acceso comodín están disponibles en el objeto EventContext.params de la función. En este ejemplo, el valor está disponible como context.params.bar.

Pueden establecerse coincidencias entre las rutas de acceso con comodines y varios eventos de una misma escritura. En el caso de la inserción de

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

existen dos coincidencias con la ruta de acceso "/foo/{bar}": una con "hello": "world" y otra con "firebase": "functions".

Administra datos de eventos

Cuando se controla un evento de Realtime Database, el objeto de datos que se muestra es una DataSnapshot. En el caso de los eventos onWrite o onUpdate, el primer parámetro es un objeto Change que contiene dos instantáneas, las cuales representan el estado de los datos antes y después del evento de activación. En el caso de los eventos onCreate y onDelete, el objeto de datos que se muestra es una instantánea de los datos creados o borrados.

En este ejemplo, la función recupera la instantánea de la ruta de acceso especificada, convierte a mayúsculas la cadena que se encuentra en esa ubicación 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);
    });

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

Desde EventContext.auth y EventContext.authType, puedes acceder a la información del usuario que activó una función, incluidos sus permisos. Esto puede ser útil para aplicar reglas de seguridad, lo que permite que tu 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, puedes aprovechar la información de autenticación del usuario para "suplantar" a un usuario y ejecutar operaciones de escritura en su nombre. Asegúrate de borrar la instancia de la app 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));
      });
    });

Lee el valor anterior

El objeto Change tiene una propiedad before que te permite inspeccionar lo que se guardó en Realtime Database antes del evento. La propiedad before muestra una DataSnapshot en la que todos los métodos (por ejemplo, val() y exists()) hacen referencia al valor anterior. Puedes volver a leer el valor nuevo si usas la DataSnapshot original o lees la propiedad after. Esta propiedad, presente en todo Change, es otra DataSnapshot que representa el estado de los datos después del evento.

Por ejemplo, se puede usar la propiedad before para garantizar que la función solo convierta el 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);
    });