Firestore から発生するエラーを HttpsError に変換する

## 概要

firestore thrown -> HttpsError への変換器を作って
ある程度汎用的に firestore のエラーをハンドリングしてしまおう。

### 背景

あるプロジェクトのサーバーサイドでは、
想定内のエラーが起きた時に HttpsError を throw する決まりとしている。
(throw された HttpsError は独自の共通処理により、内容に応じたステータスコードをレスポンスしている。)

firestore では、例えば存在しない document を update しようとした場合などにエラーを throw してくる。
しかし、 firestore にふれるすべての部分で、
想定内の firestore thrown -> HttpsError への変換を担うのはとてもコストが高く現実的ではない。
(本日時点では firestore thrown の型の定義が参照できないこともその一因だ。)

## 実装

### firestore thrown ハンドラ

```ts:firestore-helper.ts
import { HttpsError } from "firebase-functions/lib/providers/https";

export class FirestoreHelper {
  /**
   * 2020/06/10 時点では、firestore が throw するエラーの型情報にアクセスできないため、
   * HttpsError に生まれ変わらせるハンドラを作った。
   */
  public static errorHandler(error: any) {
    const isFirestoreThrown =
      error.code != null || error.details != null || error.metadata != null;

    if (!isFirestoreThrown) {
      throw error;
    }

    // Code は FirebaseFirestore.GrpcStatus に基づいているが、ここからは型参照できない
    switch (error.code) {
      case 0: //OK = 0,
        throw new HttpsError("ok", error.details);
      case 1: //CANCELLED = 1,
        throw new HttpsError("cancelled", error.details);
      case 2: //UNKNOWN = 2,
        throw new HttpsError("unknown", error.details);
      case 3: //INVALID_ARGUMENT = 3,
        throw new HttpsError("invalid-argument", error.details);
      case 4: //DEADLINE_EXCEEDED = 4,
        throw new HttpsError("deadline-exceeded", error.details);
      case 5: //NOT_FOUND = 5,
        throw new HttpsError("not-found", error.details);
      case 6: //ALREADY_EXISTS = 6,
        throw new HttpsError("already-exists", error.details);
      case 7: //PERMISSION_DENIED = 7,
        throw new HttpsError("permission-denied", error.details);
      case 8: //RESOURCE_EXHAUSTED = 8,
        throw new HttpsError("resource-exhausted", error.details);
      case 9: //FAILED_PRECONDITION = 9,
        throw new HttpsError("failed-precondition", error.details);
      case 10: //ABORTED = 10,
        throw new HttpsError("aborted", error.details);
      case 11: //OUT_OF_RANGE = 11,
        throw new HttpsError("out-of-range", error.details);
      case 12: //UNIMPLEMENTED = 12,
        throw new HttpsError("unimplemented", error.details);
      case 13: //INTERNAL = 13,
        throw new HttpsError("internal", error.details);
      case 14: //UNAVAILABLE = 14,
        throw new HttpsError("unavailable", error.details);
      case 15: //DATA_LOSS = 15,
        throw new HttpsError("data-loss", error.details);
      case 16: //UNAUTHENTICATED = 16,
        throw new HttpsError("unauthenticated", error.details);
      default:
        throw error;
    }
  }
}
```

### ハンドラの利用例

```ts:dragon.service.ts
    await firestore.runTransaction(async (tx) => {
        // do something...
    }).catch(FirestoreHelper.errorHandler); // ⭐ here!

```

```ts:dragon.service.ts
    await firestore.collection("dragons")
      .doc(dragonId)
      .update(updates)
      .catch(FirestoreHelper.errorHandler); // ⭐ here!
```

### 変換後の HttpsError 例

```
{
  "message": "No document to update: projects/<YOUR_PROJECT_NAME>/databases/(default)/documents/dragons/drag_01eac5sjgnxacznztyjs2m8fzw1",
  "status": "NOT_FOUND"
}
```

## 結論

- 強く意識しなくとも、 firestore エラーを汎用的に処理できるようになったぞ
- しかし、エラーメッセージに実装内容が滲み出てしまっているので注意だぞ
- firestore には早くエラー型の定義をしてほしいぞ