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. 参考
投稿者プロフィール

-
開発好きなシステムエンジニアです。
卓球にハマってます。
最新の投稿
【Next.js】2023年12月2日【NextJS】Local Storage
【Next.js】2023年12月2日【NextJS】Canvasを使い、図形を描画
【Next.js】2023年11月3日【NextJS】Error Handling
【Next.js】2023年10月21日【NextJS】Internationalization(i18n) – Localization