🔥

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

2022/04/22に公開

概要

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}) を使います。前者は浅い更新、後者は深い更新という違いがあります。
https://tomokazu-kozuma.com/difference-between-set-and-update-when-updating-cloud-firestore-data/

/**
 * 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 はドキュメントが存在しなくてもエラーを吐かないらしく、嫌なので存在チェックしてから叩きます。
https://stackoverflow.com/questions/53251138/firebase-firestore-returning-true-on-failed-document-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