Firestore Node.js SDKのCRUDをWrapして安全にする

32406 ワード

概要

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}) を使います。前者は浅い更新、後者は深い更新という違いがあります。