【Firebase】CloudFirestoreトリガーを使ってみる

Firebase

1. 概要

Cloud Firestoreのデータに変更があった際に、イベント処理ができる事を確認します。

2. プロジェクトを作成

こちらを参考

3. Firestoreのセットアップ

こちらを参考

4. プロジェクトのロケーションを変更

こちらを参考

5. Functionsのセットアップ

こちらを参考

6. ソースコード

6-1. functions/src/common.ts

import * as functions from "firebase-functions";

export const f = functions.region("asia-northeast1");

export const ff = functions.firestore;

6-2. functions/src/database.ts

import * as admin from "firebase-admin";

admin.initializeApp();

export const db = () => admin.firestore();

export const collectionNameUsers : string = 'users';

6-3. functions/src/types.ts

import * as admin from "firebase-admin";
import * as firestore from "firebase-admin/firestore";

export type Timestamp = admin.firestore.Timestamp | admin.firestore.FieldValue;

export type DocumentReference = firestore.DocumentReference;

export enum Sex {
    Male,
    Female
}

export enum DeleteTarget {
    Doc,
    Data
}

export enum DeleteFieldName {
    Age,
    Sex
}

6-4. functions/src/utils.ts

import * as firestore from "firebase-admin/firestore";

export const serverTimestamp = () => firestore.FieldValue.serverTimestamp();

export const fieldValueDelete = () => firestore.FieldValue.delete();

6-5. functions/src/params.ts

import * as firestore from "firebase-admin/firestore";
import * as types from "./types";

export type UserData = {
    name: {
      first: string,
      last: string
    },
    age: number,
    sex: types.Sex,
    updatedAt: types.Timestamp,
    createdAt?: types.Timestamp
};

export type UserDataForUpdate = {
    docId: string,
    merge: boolean,
    data: {
        name: {
            first: string,
            last: string
        },
        age: number,
        sex: types.Sex,
        updatedAt: types.Timestamp
    }
};

export type UserDataForDelete = {
    docId: string,
    deleteTarget: types.DeleteTarget,
    deleteFieldName: types.DeleteFieldName,
    updatedAt: types.Timestamp
}

export type DeleteField = {
    age?: number | firestore.FieldValue,
    sex?: types.Sex | firestore.FieldValue,
    updatedAt: types.Timestamp
};

6-6. functions/src/trigger.ts

import * as common from "./common";
import * as database from "./database";

export const createUser = common.ff.document(database.collectionNameUsers+'/{userId}').onCreate(async (snapshot, context) => {
  console.log('[Params] => ', context.params);
  console.log('[Create] => ', JSON.stringify(snapshot.data()));
});

export const updateUser = common.ff.document(database.collectionNameUsers+'/{userId}').onUpdate(async (change, context) => {
  console.log('[Params] => ', context.params);
  const before = change.before.data();
  const after = change.after.data();
  console.log('[Before-Update] => ', JSON.stringify(before));
  console.log('[After-Update] => ', JSON.stringify(after));
});

export const deleteUser = common.ff.document(database.collectionNameUsers+'/{userId}').onDelete(async (snapshot, context) => {
  console.log('[Params] => ', context.params);
  console.log('[Delete] => ', JSON.stringify(snapshot.data()));
});

export const writeUser = common.ff.document(database.collectionNameUsers+'/{userId}').onWrite(async (change, context) => {
  console.log('[Params] => ', context.params);
  const before = change.before.exists ? change.before.data() : null;
  const after = change.after.exists ? change.after.data() : null;
  console.log('[Before-Write] => ', JSON.stringify(before));
  console.log('[After-Write] => ', JSON.stringify(after));
});

6-7. functions/src/index.ts

import * as database from "./database";
import * as common from "./common";
import * as params from "./params";
import * as utils from "./utils";
import * as types from "./types";
export * as trigger from "./trigger";

export const getUsers = common.f.https.onRequest(async (request, response) => {
  const snapshot = await database.db().collection(database.collectionNameUsers).get();
  snapshot.forEach((user) => {
    console.log(user.id, ' => ', JSON.stringify(user.data()));
  });
  response.send('OK');
});

export const addUser = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userData : params.UserData = request.body;
    userData.updatedAt = utils.serverTimestamp();
    userData.createdAt = utils.serverTimestamp();
    try {
      await database.db().collection(database.collectionNameUsers).doc().set(userData);
    } catch (e) {
      console.error(e);
      response.status(500).send(e);
    }
    response.send('OK');
  }
});

export const setUser = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userDataForUpdate : params.UserDataForUpdate = request.body;
    console.log('userDataForUpdate => ', userDataForUpdate);
    const docId : string = userDataForUpdate.docId;
    const userData : params.UserData = userDataForUpdate.data;
    userData.updatedAt = utils.serverTimestamp();
    try {
      await database.db().collection(database.collectionNameUsers).doc(docId).set(userData, {merge: userDataForUpdate.merge});
    } catch (e) {
      console.error(e);
      response.status(500).send(e);
    }
    response.send('OK');
  }
});

export const updateUser = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userDataForUpdate : params.UserDataForUpdate = request.body;
    console.log('userDataForUpdate => ', userDataForUpdate);
    const docId : string = userDataForUpdate.docId;
    const userData : params.UserData = userDataForUpdate.data;
    userData.updatedAt = utils.serverTimestamp();
    try {
      await database.db().collection(database.collectionNameUsers).doc(docId).update(userData);
    } catch (e) {
      console.error(e);
      response.status(500).send(e);
    }
    response.send('OK');
  }
});

const deleteDoc = async (docRef: types.DocumentReference) => {
  try {
    await docRef.delete();
  } catch (e) {
    console.error(e);
  }
}

const deleteDataField = async (docRef : types.DocumentReference, deleteField : params.DeleteField) => {
  try {
    await docRef.update(deleteField);
  } catch (e) {
    console.error(e);
  }
}

export const deleteUserData = common.f.https.onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(400).send('Please request as a POST!');
  } else {
    const userDataForDelete : params.UserDataForDelete = request.body;
    console.log('userDataForDelete => ', userDataForDelete);
    const docRef = database.db().collection(database.collectionNameUsers).doc(userDataForDelete.docId);
    const deleteTarget : string = userDataForDelete.deleteTarget.toString();
    if (types.DeleteTarget[types.DeleteTarget.Doc].toString() === deleteTarget) {
      deleteDoc(docRef);
    } else {
      const deleteField: params.DeleteField = {
        updatedAt: utils.serverTimestamp()
      }
      if (types.DeleteFieldName[types.DeleteFieldName.Age].toString() === userDataForDelete.deleteFieldName.toString()) {
        deleteField.age = utils.fieldValueDelete();
      } else if (types.DeleteFieldName[types.DeleteFieldName.Sex].toString() === userDataForDelete.deleteFieldName.toString()) {
        deleteField.sex = utils.fieldValueDelete();
      } else {
        console.log('Nothing do.');
      }
      deleteDataField(docRef, deleteField);
    }
    response.send('OK');
  }
});

7. エミュレータ スイートのセットアップ

7-1. 初期化

7-2. functions/package.json

{
  "name": "functions",
  "scripts": {
    "lint": "eslint --ext .js,.ts .",
    "build": "tsc",
    "build:watch": "tsc --watch",
    "serve": "npm run build && firebase emulators:start",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "18"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^11.5.0",
    "firebase-functions": "^4.2.0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.48.2",
    "@typescript-eslint/parser": "^5.48.2",
    "eslint": "^8.32.0",
    "eslint-config-google": "^0.14.0",
    "eslint-plugin-import": "^2.27.5",
    "firebase-functions-test": "^3.0.0",
    "typescript": "^4.9.4"
  },
  "private": true
}

7-3. エミュレータを起動

cd functions
npm run serve
> serve
> npm run build && firebase emulators:start


> build
> tsc

i  emulators: Starting emulators: functions, firestore, storage
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, hosting, pubsub
✔  functions: Using node@18 from host.
i  firestore: Firestore Emulator logging to firestore-debug.log
✔  firestore: Firestore Emulator UI websocket is running on 9150.
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/home/sondon/dev/wsl/gcp/firebase/sample003/functions" for Cloud Functions...
✔  functions: Loaded functions definitions from source: trigger.createUser, trigger.updateUser, trigger.deleteUser, trigger.writeUser, getUsers, addUser, setUser, updateUser, deleteUserData.
✔  functions[us-central1-trigger-createUser]: firestore function initialized.
✔  functions[us-central1-trigger-updateUser]: firestore function initialized.
✔  functions[us-central1-trigger-deleteUser]: firestore function initialized.
✔  functions[us-central1-trigger-writeUser]: firestore function initialized.
✔  functions[asia-northeast1-getUsers]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/getUsers).
✔  functions[asia-northeast1-addUser]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/addUser).
✔  functions[asia-northeast1-setUser]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/setUser).
✔  functions[asia-northeast1-updateUser]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/updateUser).
✔  functions[asia-northeast1-deleteUserData]: http function initialized (http://127.0.0.1:5001/isub-sample-003/asia-northeast1/deleteUserData).

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000/               │
└─────────────────────────────────────────────────────────────┘

┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator  │ Host:Port      │ View in Emulator UI             │
├───────────┼────────────────┼─────────────────────────────────┤
│ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├───────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├───────────┼────────────────┼─────────────────────────────────┤
│ Storage   │ 127.0.0.1:9199 │ http://127.0.0.1:4000/storage   │
└───────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500, 9150

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

8. データの登録

8-1. 登録

curl -X POST -H "Content-Type: application/json" -d '{"name": {"first":"taro","last":"tanaka"},"age":21,"sex": "male"}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/addUser

8-2. 確認

i  functions: Beginning execution of "addUser"
i  functions: Finished "addUser" in 446.344258ms
i  functions: Beginning execution of "trigger.createUser"
>  [Params] =>  { userId: 'ewEcW2fPMAMYQx5zwyqx' }
>  [Create] =>  {"sex":"male","name":{"last":"tanaka","first":"taro"},"age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"updatedAt":{"_seconds":1675332732,"_nanoseconds":706000000}}
i  functions: Finished "trigger.createUser" in 7.675043ms
i  functions: Beginning execution of "trigger.writeUser"
>  [Params] =>  { userId: 'ewEcW2fPMAMYQx5zwyqx' }
>  [Before-Write] =>  null
>  [After-Write] =>  {"sex":"male","name":{"last":"tanaka","first":"taro"},"age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"updatedAt":{"_seconds":1675332732,"_nanoseconds":706000000}}
i  functions: Finished "trigger.writeUser" in 10.076441ms

9. データの更新

9-1. 更新

curl -X POST -H "Content-Type: application/json" -d '{"docId":"ewEcW2fPMAMYQx5zwyqx","merge":true,"data":{"name": {"first":"jiro"}}}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/setUser
  • 「taro」→「jiro」

9-2. 確認

i  functions: Beginning execution of "setUser"
>  userDataForUpdate =>  {
>    docId: 'ewEcW2fPMAMYQx5zwyqx',
>    merge: true,
>    data: { name: { first: 'jiro' } }
>  }
i  functions: Finished "setUser" in 277.027379ms
i  functions: Beginning execution of "trigger.updateUser"
>  [Params] =>  { userId: 'ewEcW2fPMAMYQx5zwyqx' }
>  [Before-Update] =>  {"sex":"male","name":{"last":"tanaka","first":"taro"},"age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"updatedAt":{"_seconds":1675332732,"_nanoseconds":706000000}}
>  [After-Update] =>  {"sex":"male","age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"name":{"last":"tanaka","first":"jiro"},"updatedAt":{"_seconds":1675333107,"_nanoseconds":498000000}}
i  functions: Finished "trigger.updateUser" in 8.583471ms
i  functions: Beginning execution of "trigger.writeUser"
>  [Params] =>  { userId: 'ewEcW2fPMAMYQx5zwyqx' }
>  [Before-Write] =>  {"sex":"male","name":{"last":"tanaka","first":"taro"},"age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"updatedAt":{"_seconds":1675332732,"_nanoseconds":706000000}}
>  [After-Write] =>  {"sex":"male","age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"name":{"last":"tanaka","first":"jiro"},"updatedAt":{"_seconds":1675333107,"_nanoseconds":498000000}}
i  functions: Finished "trigger.writeUser" in 3.15007ms

10. 削除

10-1. ドキュメントの削除

curl -X POST -H "Content-Type: application/json" -d '{"docId":"ewEcW2fPMAMYQx5zwyqx","deleteTarget":"Doc"}' http://127.0.0.1:5001/isub-sample-003/asia-northeast1/deleteUserData

10-2. 確認

i  functions: Beginning execution of "deleteUserData"
>  userDataForDelete =>  { docId: 'ewEcW2fPMAMYQx5zwyqx', deleteTarget: 'Doc' }
i  functions: Finished "deleteUserData" in 106.710941ms
i  functions: Beginning execution of "trigger.deleteUser"
>  [Params] =>  { userId: 'ewEcW2fPMAMYQx5zwyqx' }
>  [Delete] =>  {"sex":"male","age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"name":{"last":"tanaka","first":"jiro"},"updatedAt":{"_seconds":1675333107,"_nanoseconds":498000000}}
i  functions: Finished "trigger.deleteUser" in 6.971597ms
i  functions: Beginning execution of "trigger.writeUser"
>  {"severity":"WARNING","message":"Snapshot has no readTime. Using now()"}
>  [Params] =>  { userId: 'ewEcW2fPMAMYQx5zwyqx' }
>  [Before-Write] =>  {"sex":"male","age":21,"createdAt":{"_seconds":1675332732,"_nanoseconds":706000000},"name":{"last":"tanaka","first":"jiro"},"updatedAt":{"_seconds":1675333107,"_nanoseconds":498000000}}
>  [After-Write] =>  null
i  functions: Finished "trigger.writeUser" in 3.287325ms

11. 備考

シンプルな例で、データの登録・更新・削除の際にイベント処理ができる事を確認しました。

12. 参考

関連記事

  1. Firebase

    【Firebase】Cloud Firestoreデータ抽出

  2. flutter

    【Flutter】Firebase Dynamic Linksを使う

  3. flutter

    【Flutter】Cloud Firestoreと連携

  4. Firebase

    【Firebase】Cloud Firestoreを使ってみる

  5. Firebase

    【Firebase】Local Emulator Suiteを起動する…

  6. Firebase

    【Firebase】Firestore Security Rulesを…

最近の記事

  1. AWS
  2. AWS
  3. AWS
  4. AWS
  5. AWS
  6. AWS
  7. AWS

制作実績一覧

  1. Checkeys