Ampliar o Realtime Database com o Cloud Functions


Com o Cloud Functions é possível processar 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. É possível fazer alterações no Firebase Realtime Database pelo DataSnapshot ou do SDK Admin.

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

  1. Espera por alterações em um local específico do Realtime Database.
  2. É acionada quando um evento ocorre e realiza as tarefas dela. Consulte O que posso fazer com o Cloud Functions? para conferir exemplos de casos de uso.
  3. Recebe um objeto de dados que contém um snapshot dos dados armazenados em um determinado documento.

Acionar uma função do Realtime Database

Crie novas funções para eventos do Realtime Database com o functions.database. Para controlar quando a função é acionada, especifique um dos manipuladores de eventos e o caminho do Realtime Database em que ela vai detectar eventos.

Definir o manipulador de eventos

O Functions processa os eventos do Realtime Database em dois níveis de especificidade. É possível detectar especificamente apenas eventos de criação, atualização ou exclusão, ou detectar alterações de qualquer tipo em um caminho. O Cloud Functions oferece suporte aos seguintes manipuladores de eventos do Realtime Database:

  • onWrite(), que é acionado quando os dados são criados, atualizados ou excluídos do 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.

Especificar 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, 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 denominada "my-app-db-2": functions.database.instance('my-app-db-2').ref('/foo/bar')

Esses métodos direcionam sua função para tratar das gravações em um determinado caminho na instância do Realtime Database. As especificações de caminho correspondem a todas as gravações realizadas em um caminho, incluindo as que ocorrem em qualquer lugar abaixo dele. Se você definir o caminho para sua função como /foo/bar, ele vai corresponder aos eventos nestes dois 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 várias funções em caminhos mais profundos em vez de uma única função próxima à raiz do banco de dados. Para ter o melhor desempenho, solicite apenas dados no nível mais profundo possível.

Para especificar um componente do caminho como caractere curinga, basta colocá-lo entre chaves. ref('foo/{bar}') corresponde a qualquer filho de /foo. Os valores desses componentes de caminho de caracteres curinga estão disponíveis no objeto EventContext.params da sua função. Neste exemplo, o valor está disponível como context.params.bar.

Os caminhos com caracteres curinga 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: em "hello": "world" e depois em "firebase": "functions".

Processar dados de eventos

Ao processar 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 snapshots que representam o estado dos dados antes e depois do evento de acionamento. Para eventos onCreate e onDelete, o objeto de dados retornado é um snapshot dos dados criados ou excluídos.

Neste exemplo, a função recupera o snapshot do caminho especificado, converte a string no local para caixa alta e grava as modificações dela 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);
    });

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

Em EventContext.auth e EventContext.authType, é possível acessar as informações, incluindo permissões, do usuário que acionou uma função. Isso pode ser útil para aplicar regras de segurança. Assim, sua função conclui operações diferentes 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, aproveite as informações de autenticação do usuário para "representar" um usuário e executar operações de gravação em nome dele. Exclua 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));
      });
    });

Como ler 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 em que todos os métodos (por exemplo, val() e exists()) se referem ao valor anterior. Para ler o novo valor mais uma vez, use o DataSnapshot original ou leia a propriedade after. Essa propriedade em qualquer Change será outro DataSnapshot que representa o estado dos dados depois que o evento aconteceu.

Por exemplo, a propriedade before pode ser usada para garantir que a função só transcreva texto em caixa alta quando ele é criado 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);
    });