Cloud Functions トリガーonWriteの「change」と「context」について


はじめに

Cloud Functionsのトリガー「onWrite」を使ったときに、引数で受け取る2つの値がある。
それが「change」と「context」である。

index.ts
const functions = require('firebase-functions');

exports.useWildcard = functions.firestore
    .document('users/{userId}')
    .onWrite((change, context) => {
      // do something
    });

でも、この「change」と「context」が一体何なのか?
これが最初はよくわからないですよね〜

前提となるトリガーonWriteについて

そもそもonWriteとは?というと「onCreate、onUpdate または onDelete がトリガーされたときにトリガーされる」というものです。

つまり、データに何かしらの変更があったときに発動するトリガー、との認識で大丈夫かと!

「change」は対象のドキュメントのデータを取得する

まずは「change」から。

ドキュメントによると

関数がトリガーされた後、更新済みのドキュメントのデータを取得する場合や、更新前のデータを取得する場合があります。更新前のデータは、更新前のドキュメントのスナップショットを含む change.before.data() を使用して取得できます。同様に、change.after.data() には更新後のドキュメントのスナップショットの状態が含まれます。
https://firebase.google.com/docs/functions/firestore-events?hl=ja

うーん、わかりにくい。。。

実例で解説すると

users/{userId}/events/{eventId}の中に、以下のようなドキュメントがあったとする

exports.updateUser2 = functions.firestore
    .document('users/{userId}/events/{eventId}')
    .onUpdate((change, context) => {
      // Get an object representing the current document
      const newValue = change.after.data('title');

      // ...or the previous value before this update
      const previousValue = change.before.data('title');
    });

①もし、「title=沖縄」で新規のドキュメントを追加した場合
newValueは「沖縄」となる
previousValueは「undefined」となる

②もし既存のドキュメント(①で作成したもの)を「title=大阪」で更新した場合
newValueは「大阪」となる
previousValueは「沖縄」となる

なぜ①previousValueが「undefined」なのかというと、新規で作成されたドキュメントなので変更前(change.before)が存在しないからです。

いやーこれに気がついた時は、すっきりしました

「context」も対象のドキュメントのデータを取得する

「context」の最もよく使われるであろう用途はずばりワイルドカード!

ドキュメントによると

ワイルドカードに一致した部分がドキュメント パスから抽出され、context.params に保存されます。明示的なコレクションまたはドキュメント ID に置き換えるワイルドカードは、必要な数だけ定義できます。
https://firebase.google.com/docs/functions/firestore-events?hl=ja

微妙にわからない・・・

実例で解説すると

exports.countEvents = functions.firestore
.document('users/{userId}/events/{eventId}')
  .onWrite((change, context) => {

    const userId = context.params.userId;
    const FieldValue = admin.firestore.FieldValue;
    const countsRef = db.collection('users').doc(userId);

    if (!change.before.exists) {
      // 登録時に件数をインクリメント
      return countsRef.update({ eventCount: FieldValue.increment(1) } );
    } else if (change.before.exists && !change.after.exists) {
      // 削除時に件数をデクリメント
      return countsRef.update({ eventCount: FieldValue.increment(-1) });
    }
    return;
  });

上記のコードは「イベントを追加したら、その数をカウントをする」というような処理をしています。
(上記の処理について、詳しくはこちらを参考に)

どのユーザーがイベントを追加したのか?を知るためにはuidを取得する必要があります。
なので
const userId = context.params.userId;
の部分で、イベントを追加したuserのuidを取得しています。

では「context.params.userId」のparamsの後の「userId」はどうやって決まったのか?
「uid」ではダメなのか?

exports.countEvents = functions.firestore
.document('users/{userId}/events/{eventId}')

上記のコードの場合、paramsの後は「uid」ではダメです。
なぜなら、.document('users/{userId}/events/{eventId}')のパスの中で、「userId」をワイルドカードにしているからです。

逆言えば

exports.countEvents = functions.firestore
.document('users/{uid}/events/{eventId}')

この場合は.document('users/{uid}/events/{eventId}')のパスの中で、「uid」が使われてるので、
const userId = context.params.uid;

にしないとuserのIDは取得できません。

最後に

contextについてはワイルドカード以外に使ったことがなく、あんまり詳しくないので

より詳しく知りたい方は下記を参考に・・・
https://firebase.google.com/docs/reference/functions/cloud_functions_.eventcontext?hl=ja

こちらは日本語記事
https://blog.firstfournotes.com/tech/firebase-follow-followered/