Cloud Functions for Firebase
index.ts
グローバルオプションの設定
関数ごとにリージョンを設定するのは大変なので、ここで一括で設定できる
デフォルトのタイムアウトは60秒
デフォルトのメモリ設定は256MB
setGlobalOptions({
region: 'asia-northeast1',
timeoutSeconds: 540,
memory: '2GiB',
});
タイムゾーンの設定
関数を日本時刻で実行できるようになる
process.env.TZ = 'Asia/Tokyo';
utilsディレクトリ
複数の場所で使用される関数を格納するために使われる
extractMatchingAnalytics.tsがutilsディレクトリの下にある理由
extractMatchingAnalyticsは特定の役割に特化しているわけではなく幅広く使える関数であるため
extractMatchingAnalytics.ts
UTC(協定世界時)からJST(日本標準時)へ
9時間 × 60分 × 60秒 × 1000ミリ秒
const jstDate = new Date(now.getTime() + 9 * 60 * 60 * 1000);
月を2桁表示させる
1月→0,12月→11なので1を加える
頭に0をつけてsliceで後ろから2文字を取ると1桁の場合にだけ十の位に0がつく
const month = ('0' + (jstDate.getUTCMonth() + 1)).slice(-2);
Intl.DateTimeFormatOptions
日付と時刻のフォーマット方法を指定できる
const options: Intl.DateTimeFormatOptions = {
year: 'numeric', // 年を数値で表示する
month: '2-digit', // 月を2桁で表示する
day: '2-digit', // 日を2桁で表示する
timeZone: 'Asia/Tokyo' // タイムゾーンを日本時間にする
};
db.bulkWriter()
Firebase Firestoreの大量の書き込みを効率的に行うための機能
それぞれの書き込み操作に対してエラーハンドリングをカスタマイズすることもできる
失敗した書き込み操作の再試行もできる
const bulkWriter = db.bulkWriter();
// エラーハンドリングの設定
bulkWriter.onWriteError((error) => {
console.error('Error writing document:', error);
return error.failedAttempts < 5; // 失敗回数が5回未満の場合、再試行する
});
// ドキュメントに対する書き込み
const docRef = db.collection('articles').doc('article-id');
bulkWriter.set(docRef, { title: 'New Title' });
// 書き込みの終了
await bulkWriter.close();
- bulkWriter.set(): 書き込みを行う。ドキュメントが存在しない場合は新しいドキュメントを作成
- bulkWriter.update(): 既存のドキュメントを更新
- bulkWriter.delete(): ドキュメントを削除
- bulkWriter.flush(): 保留中のすべての書き込みを実行
- bulkWriter.close(): すべての操作を完了してバルクライターを閉じる
Date.toISOString()
日付と時刻をISO8601形式の文字列に変換
ISO8601形式はYYYY-MM-DDTHH:mm:ss.sssZ
と表す
date.toISOString().slice(0, 7).replace('-', '');
date.toISOString()
でYYYY-MM-DDTHH:mm:ss.sssZ
slice(0, 7)
でYYYY-MM
replace('-', '')
でYYYYMM
new Date(Date.now() - 86400000)
Date.now() - 86400000
は現在の時間から86400000ミリ秒(24時間)を引いてるので前日の日付を取得
Firebase Firestoreのデータベースから特定のドキュメントを検索
コレクションとドキュメントへの参照
db.collection('articles').doc(doc.id).collection('dateData')
コレクションを参照
db.collection('articles'))
はarticlesというコレクションを参照
ドキュメントを参照
.doc(doc.id)
はarticlesコレクションの中のドキュメントを参照
doc.id
は各ドキュメントのID
サブコレクションを参照
.collection('dateData')
はdoc.idドキュメントの中にあるサブコレクションdateData
を参照
検索条件を設定
.where(FieldPath.documentId(), '>=', targetMonth)
.where(FieldPath.documentId(), '<', nextMonth)
.where(FieldPath.documentId(), '>=', targetMonth)
はdateDataサブコレクション内のドキュメントIDがtargetMonth以上のものをフィルタリング
.where(FieldPath.documentId(), '<', nextMonth)
は、dateDataサブコレクション内のドキュメントIDがnextMonthより小さいものをフィルタリング
つまりtargetMonthからnextMonthまでの範囲にあるすべてのドキュメントを取得する
実行
検索条件に基づいてデータベースにリクエストを送る
.get();
Promise.all
複数のPromiseを並行して実行し、すべてが完了するまで待機
すべてのPromiseが解決されると、それぞれのPromiseの結果を含む配列を返す
Promise.allを使うとき
- 複数の非同期操作を並行して実行したいとき
複数のAPIリクエストを同時に行い、すべてのリクエストが完了した後で次の処理を実行する - 複数のPromiseの結果をまとめて取得したいとき
すべてのPromiseが解決された後で、その結果を一度に処理する
エラーハンドリング
Promiseのうち1つでも拒否された場合、Promise.allはすぐに拒否し、残りのPromiseの結果を待たない
使用例
Promise.all([
getSheetData(sheets, dbSheetID, 'access'),
getSheetData(sheets, dbSheetID, 'cv'),
getSheetData(sheets, dbSheetID, 'session'),
getSheetData(sheets, dbSheetID, 'landing'),
getSheetData(sheets, dbSheetID, 'clicks'),
getSheetData(sheets, dbSheetID, 'ctr'),
getSheetData(sheets, dbSheetID, 'impressions'),
getSheetData(sheets, dbSheetID, 'position'),
]),
Promise.allを使うことで複数のgetSheetData関数を並行して実行することができ、処理時間を短縮できる
すべてのPromiseが解決されてから次に進むので、データの欠如を防げる
シートのバッチ更新
Google Sheetsに対して複数の変更を一度にまとめて行う操作のこと
バッチ更新のメリット
- 処理速度の向上
- APIの制限回避
スプレッド構文
const monthsWithID = ['ID', ...months.sort()];
先頭に'ID'という文字列を追加し、months.sort()でアルファベット順にソート
...スプレッド構文を使ってソートされた月名を新しい配列に展開
A列に「ID」という項目があるため、先頭に'ID'という文字列を追加を追加している
sheets.spreadsheets.values.getとsheets.spreadsheets.get
const sheet = await sheets.spreadsheets.values.get({
spreadsheetId,
range,
});
const sheetMetadata = await sheets.spreadsheets.get({
spreadsheetId,
});
spreadsheetIdを2回使って何を取得しようとしているのか
sheets.spreadsheets.values.get
特定の範囲(range)のセルデータを取得している
シート内のセルデータ、つまり値のみ
sheets.spreadsheets.get
スプレッドシート全体のメタデータを取得している
各シートのIDや名前など
結論
spreadsheetIdを2回使ってセルデータとシートのメタデータを取得しようとしている
メタデータからシートIDを検索する
const sheetID = sheetMetadata.data.sheets?.find((s) => s?.properties?.title === range);
sheetMetadata.data.sheets
sheetMetadataにはスプレッドシート内にある全てのシートの情報が含まれる
複数のシートを扱っているため、sheetMetadata.data.sheetsはシートのリスト(配列)を表している
例
[
{ properties: { title: 'Sheet1', sheetId: 12345 } },
{ properties: { title: 'Sheet2', sheetId: 67890 } },
]
オプショナルチェーン ?.
sheetMetadata.data.sheetsがundefinedやnullの場合、エラーを出さずにそのままundefinedを返す
find()
配列の中から条件に一致する最初の要素を返す
s?.properties?.title === range
各シートのproperties.titleが、range(シート名)と一致するかを確認
rangeが"Sheet2"の場合、titleが"Sheet2"のシートを探す
sheetID
.find()によってrangeに一致するシートのオブジェクトが返される
{ properties: { title: 'Sheet2', sheetId: 67890 } }
なぜrangeが必要なのか?titleだけではだめなのか?
まず、sheets.spreadsheets.values.getでtitleを取得することはできないのでrangeを取得する
rangeは'Sheet1!A1:B10'というふうに範囲を指定できるが、'Sheet1'とするとそのシート全体を指定できる
今回はrangeでシート全体を指定しているので、rangeとtitleが一致しfind()で検索してそのシートのIDを取得している
masterFlag
return masterFlag ? sheet.data.values : { sheet: sheet.data.values, sheetID: sheetID.properties?.sheetId };
masterFlagがtrueの場合
sheet.data.valuesを返す
シートのセルの内容のみ
masterFlagがfalseの場合
シートのセルの内容とシートIDを返す
結論
シートIDが不要であればtrue
必要であればfalseにして使い分ける
Jestでテストを行うときにメモリが不足しているときの対処法
node --max-old-space-size=4096 node_modules/.bin/jest
メモリサイズを大きくする
--max-old-space-size=4096
はメモリサイズを4GBにする
デフォルトでは約1.5GB
Jestを実行
node_modules/.bin/jest
でJestを実行
このあとにファイルを指定するとそのファイルのテストが行える
Jestでテストを行う
現在の時刻の1日前の日付を取得し、指定した形式に変換する関数のテストを行う
関数をimport
テストを行いたい関数をimportする
テストしたい部分を関数として切り出しておくとよい
import { getFormattedDate } from '../../../src/v1/utils/extractMatchingAnalytics';
テストを定義
どの関数のテストを行うか定義する
describe('[Fn] getFormattedDate', () => {
フェイクタイマーを起動させる
テストを行うときは時刻を操作したいのでテストの前にフェイクタイマーを起動させる
beforeEach(() => {
jest.useFakeTimers();
});
テストが終わったらリアルタイマーに戻す
テストが終了したらリアルタイムのタイマーに戻す
afterEach(() => {
jest.useRealTimers();
});
複数のテストケースの定義
it.each
で複数のテストケースを一気に行える
currentDate
に時刻、expected
に期待されるテスト結果
it.each`
currentDate | expected
${'2024-09-19T00:00:00Z'} | ${'20240918'}
${'2024-09-19T15:00:00Z'} | ${'20240919'}
${'2024-09-01T06:00:00Z'} | ${'20240831'}
${'2024-09-01T15:00:00Z'} | ${'20240901'}
${'2024-01-01T12:00:00Z'} | ${'20231231'}
${'2023-12-31T15:00:00Z'} | ${'20231231'}
`
テスト行う
時間を設定し、getFormattedDate()関数を呼び出して結果とexpectedが一致するかを確かめる
('Current date is $currentDate', ({ currentDate, expected }) => {
const date = new Date(currentDate);
jest.setSystemTime(date);
const result = getFormattedDate();
expect(result).toBe(expected);
});
beforeAllとafterAll
beforeAll
beforeAllはdescribe内の全てのテストが実行される前に一度だけ実行
一度だけ実行すればよい初期設定などで使う
afterAll
afterAllはdescribe内の全てのテストが実行されたあとに一度だけ実行
一度だけ実行すればよい後処理がある場合に使う
beaforeEachとbeforeAllの違い
beaforeEachは各テストケース(itやtest関数)の前に毎回実行される
afterEachも同様に各テストケースの後に毎回実行される
itが2個ある場合(テスト1とテスト2)
beforeAll→beaforeEach→テスト1→afterEach→beaforeEach→テスト2→afterEach→afterAll