Firestore Node.js SDKのCRUDをWrapして安全にする
概要
Firestore SDKの set
、恐ろしいですよね。insertのつもりで間違えて存在するidを指定してしまうと何の音も立てずにドキュメントを踏み潰します。既に存在したらエラーになる関数が欲しい。
あと返り値が Promise<void>
なのも気になります。僕は挿入した結果のオブジェクトが返ってほしい派です({success: true}とかでもいいけど)。
というわけでFirestoreのCRUDをWrapしていくという記事です。
痒いところに手が届かない
Firestoreと、参考までにSQL(MySQL)のINSERT/UPDATE周りの仕様を見てみます。
function | ドキュメントが存在しない | ドキュメントが存在する | 理想 |
---|---|---|---|
firestore set | 生成 | 上書き | |
firestore set({merge: true}) | 生成 | field更新(深) | UPSERT |
firestore update | エラー | field更新(浅) | |
firestore add | 生成(自動採番) | - | |
SQL INSERT | 生成 | (同一のものが)生成 | |
SQL INSERT with PRIMARY KEY | 生成 | エラー | INSERT |
SQL UPDATE | 何も起きない | field(カラム)更新 | |
エラー | field更新(深) | UPDATE |
カオス。
僕がFirestoreの読み書きに求めたいものは"理想"に書いてあるものたちです。
これらを実現するために、諸々の関数をwrapした最強の関数を作ってみましょう。
実装
get
まず get
をwrapする関数を書きました。ジェネリクスの type T
は、オブジェクトのプロパティとして id
を持つことにしました(別々なのがめんどくさいので)(元々idっていうプロパティがあったら詰む)。
また、無いdocをgetした時にnullが返るようにしました。
export const getDocument = async <T>(collection: string, documentId: ID, firestore: any) : Promise<T> => {
const ref = firestore.collection(collection).doc(documentId);
const data = await ref.get().then((doc: any) => {
if (doc.exists) {
const docData = doc.data();
docData.id = doc.id;
return docData;
}else{
return null
}
}).catch(function(error: any) {
console.log("Error getting document: ", error);
});
return data;
}
INSERT
get
-> set
-> get
の順で3回叩いています。呼び出し回数増えるのは仕方なし。
transactionは無視しています。set({merge: true})
なので被ってもそんなにやばいことにはならないはず。
/**
* INSERT (ない時作成、ある時エラー、返り値はINSERTされた値)
* @param collection
* @param documentId
* @param object
* @param firestore
*/
export const insertDocument = async <T>(collection: string, documentId: string, object: object, firestore: Firestore) : Promise<T> => {
const ref = await firestore.collection(collection).doc(documentId);
await ref.get().then(async (doc: any) => {
if (doc.exists) {
throw documentId + " already exists.";
} else {
return await ref.set(object, {merge: true});
}
}).catch(function(error: string) {
throw error;
});
return await getDocument<T>(collection, documentId, firestore);
}
INSERT(自動採番)
add
はドキュメントIDを自動生成してくれるので便利。
/**
* INSERT (自動採番、返り値はINSERTされた値(id付き))
* @param collection
* @param object
* @param firestore
*/
export const autoInsertDocument = async <T>(collection: string, object: Omit<T, "id">, firestore: Firestore) : Promise<T> => {
const ref = await firestore.collection(collection).add(object);
const res = await ref.get().then(async (doc: any) => {
const data = Object.assign(doc.data(), {id: doc.id})
return data
}).catch(function(error: string) {
throw error;
});
return res;
}
UPDATE
こちらも update
ではなく set({merge: true})
を使います。前者は浅い更新、後者は深い更新という違いがあります。
/**
* UPDATE (ない時エラー、ある時深いfield更新、返り値はUPDATEされた値)
* @param collection
* @param documentId
* @param updates
* @param firestore
*/
export const updateDocument = async <T>(collection: string, documentId: ID, updates: object, firestore: Firestore) : Promise<T> => {
const ref = firestore.collection(collection).doc(documentId);
await ref.get().then(async (doc: any) => {
if (doc.exists) {
return await ref.set(updates, {merge: true});
} else {
throw documentId + " does not exist.";
}
}).catch(function(error: string) {
throw error;
});
return await getDocument<T>(collection, documentId, firestore);
}
UPSERT
「あればUPDATE、なければINSERT」はUPSERTという名前があります。
これは完全に set({merge: true})
です。
/**
* UPSERT (ない時作成、ある時深いfield更新、返り値はUPSERTされた値)
* @param collection
* @param documentId
* @param updates
* @param firestore
*/
export const upsertDocument = async <T>(collection: string, documentId: ID, updates: object, firestore: Firestore) : Promise<T> => {
const ref = firestore.collection(collection).doc(documentId);
await ref.set(updates, {merge: true});
return await getDocument<T>(collection, documentId, firestore);
}
DELETE
delete
はドキュメントが存在しなくてもエラーを吐かないらしく、嫌なので存在チェックしてから叩きます。
/**
* DELETE (ない時エラー、ある時削除、返り値は消される前の値)
* @param collection
* @param documentId
* @param firestore
*/
export const deleteDocument = async <T>(collection: string, documentId: ID, firestore: Firestore) : Promise<T> => {
const ref = firestore.collection(collection).doc(documentId);
await ref.get().then(async (doc: any) => {
if (doc.exists) {
ref.delete();
return await doc.data();
} else {
throw documentId + " does not exist.";
}
}).catch(function(error: string) {
throw error;
});
return await getDocument<T>(collection, documentId, firestore);
}
まとめ
- 使いにくい関数はwrapしてしまいましょう。
- 次はSQL編をお届けします。
Discussion