package.json
{
"firebase": "^9.0.2",
"firebase-admin": "^9.11.1",
}
client
- v9ではバンドルサイズを最小限にするために、関数ベースに切り替わっている。
// db.collection("...").where("...") // 以前はこう出来ていたが
query(db, where("..."), limit("...")) // v9ではこういう感じ
typescript
import {
YourCollectionInterface
} from "..."
import { getApp, getApps, initializeApp } from "firebase/app"
import {
collection,
FirestoreDataConverter,
getFirestore,
QueryDocumentSnapshot,
WithFieldValue
} from "firebase/firestore"
const firebaseConfig = { ... }
const apps = getApps()
const app = apps.find(({ name }) => name == "client")
? getApp("client")
: initializeApp(firebaseConfig, "client")
const firestore = getFirestore(app)
const converter = <T>(): FirestoreDataConverter<T> => ({
toFirestore: (data: WithFieldValue<T>) => data,
fromFirestore: (snapshot: QueryDocumentSnapshot<T>) => {
const data = snapshot.data()
Object.keys(data).forEach((key) => {
if (
typeof data[key].toString == "function" &&
data[key].toString().startsWith("Timestamp")
) {
// firestoreのtimestamp型は扱いにくいのでdate型に変更させる
data[key] = data[key].toDate()
}
})
return data
},
})
const dataPoint = <T>(collectionPath: string) =>
collection(firestore, collectionPath).withConverter(converter<T>())
const db = {
yourCollectionInterface: dataPoint<YourCollectionInterface>("..."),
}
export { firestore, db }
YourCollectionInterface.ts
export interface YourCollectionInterface {
name: string
value: number
createdAt: Date // firestore側がTimestamp型でもconverterで強制的にDateに変えている
}
こうすることで、以下のように出来る。
import { db } from "..."
import {
getDocs,
query,
where,
orderBy,
} from "firebase/firestore"
const q = query(
db.yourCollectionName,
where("..."),
orderBy("...")
)
const snapshot = await getDocs(q)
const records = snapshot.docs.map((doc) => doc.data())
// ここでコード補完が効く
records[0].name
records[0].value
records[0].createdAt // Date型
server
- サーバーなのでバンドルサイズは気にしなくていいので、v8以前今まで通りのクラスベースのまま。
- クライアントからはアクセスを制限しているデータにアクセスできる。
typescript
import * as admin from "firebase-admin"
import {
YourCollectionInterface,
} from "."
const firebaseConfig = { ... }
// https://firebase.google.com/docs/admin/setup?hl=ja
// GOOGLE_APPLICATION_CREDENTIALS を使っても良い
// vercelに置く場合などは、GOOGLE_APPLICATION_CREDENTIALSでパスの指定が困難なので直接読み込んだほうが良いと思う
const serviceAccount = require("firebase-key.json")
if (!admin.apps.find(({ name }) => name == "server")) {
admin.initializeApp(
{
credential: admin.credential.cert(serviceAccount),
databaseURL: firebaseConfig.databaseURL,
},
"server"
)
}
const firestore = admin.app("server").firestore()
const FirebaseFirestore = admin.firestore // FirebaseFirestore.Timestamp.fromDate(...) とかするときに便利
const converter = <T>(): FirebaseFirestore.FirestoreDataConverter<T> => ({
toFirestore: (data: T) => data,
fromFirestore: (snapshot: FirebaseFirestore.QueryDocumentSnapshot<T>) => {
const data = snapshot.data()
Object.keys(data).forEach((key) => {
if (typeof data[key].toDate == "function" && typeof data[key].seconds == "number") {
// Timestamp型は扱いにくいのでDate型に強制変更
data[key] = data[key].toDate()
}
})
return data
},
})
const dataPoint = <T>(collectionPath: string) =>
firestore.collection(collectionPath).withConverter(converter<T>())
const db = {
yourCollectionInterface: dataPoint<YourCollectionInterface>("..."),
}
export { FirebaseFirestore, db }
こうすることで
const snapshot = await db.yourCollectionInterface
.where("createdAt", ">", FirebaseFirestore.Timestamp.fromDate(...))
.orderBy("createdAt", "desc")
.get()
const records = snapshot.docs
.map((doc) => doc.data())
// コード補完が効く
records[0].name
records[0].value
records[0].createdAt // Date型
next.js での注意点
- サーバーサイド用の
firebase-admin
はクライアント側のコード(最終的にクライアント側に流されるコード部分)で使わない - 使ってしまうと、多分
Module not found: Can't resolve 'fs'
のエラーが出る
next.js getStaticProps、getServerSidePropsでの注意点
Reason: `object` ("[object Date]") cannot be serialized as JSON.
Please only return JSON serializable data types.
Timestamp型はもちろん、Date型もpropsとして渡そうとするとJSON serializableじゃないと言ってエラーになる。
- TimestampはDate型にしないとpropsで渡すのは無理そう
- Date型は本来JSON serializableなんだけど、next.jsはエラーになる
解決策
superjsonを使う。readme通りにやればいい。