Cloud Functions による Realtime Database の拡張


Cloud Functions を使用すると、クライアント コードを更新することなく、Firebase Realtime Database 内のイベントを処理できます。 Cloud Functions では、完全な管理者権限のもとに Realtime Database オペレーションを実行できます。また、Realtime Database に対する個々の変更はそれぞれ個別に処理されます。Firebase Realtime Database の変更は、DataSnapshot または Admin SDK を使用して行うことができます。

一般的なライフサイクルの場合、Firebase Realtime Database の関数は次のように動作します。

  1. 特定の Realtime Database のロケーションに変更が加えられるのを待ちます。
  2. イベントが発生するとトリガーされ、そのタスクを実行します(ユースケースについては、Cloud Functions で可能な処理をご覧ください)。
  3. 指定されたドキュメントに保存されているデータのスナップショットを含むデータ オブジェクトを受け取ります。

Realtime Database 関数をトリガーする

functions.database を使用して、Realtime Database イベント用の新しい関数を作成します。関数がトリガーされるタイミングを制御するには、イベント ハンドラの 1 つを指定し、イベントをリッスンする Realtime Database パスを指定します。

イベント ハンドラを設定する

関数で Realtime Database イベントを処理するにあたり、リッスン対象とするイベントを 2 つのレベルで指定できます。作成、更新、削除の各イベント限定、そしてパスに対するあらゆる種類の変更です。Cloud Functions は、Realtime Database の次のイベント ハンドラをサポートしています。

  • onWrite() - Realtime Database 内でデータが作成、更新、削除されるとトリガーされます。
  • onCreate() - Realtime Database 内で新しいデータが作成されるとトリガーされます。
  • onUpdate() - Realtime Database 内でデータが更新されるとトリガーされます。
  • onDelete() - Realtime Database からデータが削除されるとトリガーされます。

インスタンスとパスを指定する

関数がトリガーされるタイミングと場所を制御するには、ref(path) を呼び出してパスを指定します。必要に応じて instance('INSTANCE_NAME') を使用して Realtime Database インスタンスを指定します。インスタンスを指定しない場合、関数は Firebase プロジェクトのデフォルトの Realtime Database インスタンスにデプロイされます。次に例を示します。

  • デフォルトの Realtime Database インスタンス: functions.database.ref('/foo/bar')
  • 名前が「my-app-db-2」のインスタンス: functions.database.instance('my-app-db-2').ref('/foo/bar')

このメソッドは、Realtime Database インスタンス内の特定のパスで書き込みを処理するよう関数に指示します。パスを指定すると、そのパスに関係するすべての書き込みが対象となり、これにはそのパス下のあらゆる場所で発生する書き込みが含まれます。関数のパスを /foo/bar として設定すると、次の場所のイベントはいずれも一致対象となります。

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

どちらの場合も、Firebase ではイベントが /foo/bar で発生したと解釈され、イベントデータには /foo/bar の古いデータと新しいデータが含まれます。イベントデータが大きい場合は、データベースのルート付近で単一の関数を使用する代わりに、より深いパスで複数の関数を使用することを検討してください。最高のパフォーマンスを得るには、できるだけ深いレベルのデータのみを要求するようにします。

パス コンポーネントは、中かっこで囲むことでワイルドカードとして指定できます。ref('foo/{bar}')/foo のすべての子と一致します。このワイルドカード パス コンポーネントの値は、関数の EventContext.params オブジェクト内で使用できます。この例では、値は context.params.bar として使用可能になります。

ワイルドカードを含むパスは、1 つの書き込みの複数のイベントに一致する場合があります。次のように挿入したとします。

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

これはパス "/foo/{bar}" と 2 回一致します。1 回目は "hello": "world" で、2 回目は "firebase": "functions" です。

イベントデータを処理する

Realtime Database イベントを処理するときに返されるデータ オブジェクトは DataSnapshot です。 onWrite イベントまたは onUpdate イベントの場合、最初のパラメータは、トリガーとなるイベントの前後のデータ状態を表す 2 つのスナップショットを含む Change オブジェクトです。onCreate イベントと onDelete イベントの場合、返されるデータ オブジェクトは、作成または削除されたデータのスナップショットです。

この例では関数は指定されたパスのスナップショットを取得し、その場所の文字列を大文字に変換して、変更された文字列をデータベースに書き込みます。

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

ユーザー認証情報にアクセスする

EventContext.authEventContext.authType から関数をトリガーしたユーザーの情報(権限など)にアクセスできます。これは、セキュリティ ルールを適用するのに便利です。ユーザーの権限レベルに基づいて関数で異なるオペレーションを実行できます。

const functions = require('firebase-functions/v1');
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);
      }
    });

ユーザー認証情報を使用して、ユーザーの「権限を借用」し、ユーザーの代わりに書き込みオペレーションを実行することもできます。同時実行の問題を回避するために、次に示すように必ずアプリのインスタンスを削除してください。

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

前の値を読み取る

Change オブジェクトの before プロパティを使用すると、イベントの発生Realtime Database に保存されていた内容を調べることができます。before プロパティによって返される DataSnapshot では、すべてのメソッド(val()exists() など)が前の値を参照します。新しい値を再度読み取るには、元の DataSnapshot を使用するか、after プロパティを読み取ります。Change のこのプロパティによって返される DataSnapshot は、イベント発生のデータの状態を表します。

たとえば、before プロパティを使用して、データが最初に作成されたときにのみテキストを大文字にできます。

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