Estenda o Realtime Database com Cloud Functions


Com o Cloud Functions, você pode gerenciar eventos no Firebase Realtime Database sem precisar atualizar o código do cliente. O Cloud Functions permite executar operações do Realtime Database com privilégios administrativos totais e garante que cada alteração no Realtime Database seja processada individualmente. Você pode fazer alterações no Firebase Realtime Database por meio do DataSnapshot ou do Admin SDK .

Em um ciclo de vida típico, uma função do Firebase Realtime Database faz o seguinte:

  1. Aguarda alterações em um local específico do Realtime Database.
  2. É acionado quando um evento ocorre e executa suas tarefas (consulte O que posso fazer com o Cloud Functions? para ver exemplos de casos de uso).
  3. Recebe um objeto de dados que contém um instantâneo dos dados armazenados no documento especificado.

Acionar uma função do Realtime Database

Crie novas funções para eventos do Realtime Database com functions.database . Para controlar quando a função é acionada, especifique um dos manipuladores de eventos e o caminho do Realtime Database onde ele escutará os eventos.

Defina o manipulador de eventos

As funções permitem lidar com eventos do Realtime Database em dois níveis de especificidade; você pode escutar especificamente apenas eventos de criação, atualização ou exclusão, ou pode escutar qualquer alteração de qualquer tipo em um caminho. O Cloud Functions oferece suporte a estes manipuladores de eventos para Realtime Database:

  • onWrite() , que é acionado quando os dados são criados, atualizados ou excluídos no Realtime Database.
  • onCreate() , que é acionado quando novos dados são criados no Realtime Database.
  • onUpdate() , que é acionado quando os dados são atualizados no Realtime Database .
  • onDelete() , que é acionado quando os dados são excluídos do Realtime Database .

Especifique a instância e o caminho

Para controlar quando e onde sua função deve ser acionada, chame ref(path) para especificar um caminho e, opcionalmente, especifique uma instância do Realtime Database com instance('INSTANCE_NAME') . Se você não especificar uma instância, a função será implantada na instância padrão do Realtime Database para o projeto do Firebase. Por exemplo:

  • Instância padrão do Realtime Database: functions.database.ref('/foo/bar')
  • Instância chamada "my-app-db-2": functions.database.instance('my-app-db-2').ref('/foo/bar')

Esses métodos direcionam sua função para lidar com gravações em um determinado caminho na instância do Realtime Database. As especificações de caminho correspondem a todas as gravações que tocam um caminho, incluindo gravações que acontecem em qualquer lugar abaixo dele. Se você definir o caminho para sua função como /foo/bar , ele corresponderá aos eventos em ambos os locais:

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

Em ambos os casos, o Firebase interpreta que o evento ocorre em /foo/bar , e os dados do evento incluem os dados antigos e novos em /foo/bar . Se os dados do evento forem grandes, considere usar diversas funções em caminhos mais profundos, em vez de uma única função próxima à raiz do seu banco de dados. Para obter o melhor desempenho, solicite dados apenas no nível mais profundo possível.

Você pode especificar um componente de caminho como um curinga colocando-o entre colchetes; ref('foo/{bar}') corresponde a qualquer filho de /foo . Os valores desses componentes de caminho curinga estão disponíveis no objeto EventContext.params da sua função. Neste exemplo, o valor está disponível como context.params.bar .

Caminhos com curingas podem corresponder a vários eventos de uma única gravação. Uma inserção de

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

corresponde ao caminho "/foo/{bar}" duas vezes: uma vez com "hello": "world" e novamente com "firebase": "functions" .

Lidar com dados de eventos

Ao manipular um evento do Realtime Database, o objeto de dados retornado é um DataSnapshot . Para eventos onWrite ou onUpdate , o primeiro parâmetro é um objeto Change que contém dois instantâneos que representam o estado dos dados antes e depois do evento acionador. Para eventos onCreate e onDelete , o objeto de dados retornado é um instantâneo dos dados criados ou excluídos.

Neste exemplo, a função recupera o instantâneo para o caminho especificado, converte a string naquele local em letras maiúsculas e grava essa string modificada no banco de dados:

// 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);
    });

Acessando informações de autenticação do usuário

Em EventContext.auth e EventContext.authType , você pode acessar as informações do usuário, incluindo permissões, para o usuário que acionou uma função. Isso pode ser útil para impor regras de segurança, permitindo que sua função conclua diferentes operações com base no nível de permissões do usuário:

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);
      }
    });

Além disso, você pode aproveitar as informações de autenticação do usuário para "representar" um usuário e executar operações de gravação em nome do usuário. Certifique-se de excluir a instância do aplicativo conforme mostrado abaixo para evitar problemas de simultaneidade:

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));
      });
    });

Lendo o valor anterior

O objeto Change tem uma propriedade before que permite inspecionar o que foi salvo no Realtime Database antes do evento. A propriedade before retorna um DataSnapshot onde todos os métodos (por exemplo, val() e exists() ) referem-se ao valor anterior. Você pode ler o novo valor novamente usando o DataSnapshot original ou lendo a propriedade after . Esta propriedade em qualquer Change é outro DataSnapshot que representa o estado dos dados após a ocorrência do evento.

Por exemplo, a propriedade before pode ser usada para garantir que a função apenas coloque texto em maiúscula quando for criada pela primeira 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);
    });