nest.jsでFirebase Adminの初期設定とCRUD処理

nest.jsのFirestoreのCRUD
◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
VSCode Dev Containersで楽々開発環境構築祭り〜Python/Reactなどなど〜
Visual Studio Codeの拡張機能であるDev Containersを使ってReactとかPythonとかSpring Bootとかの開発環境をラクチンで構築する方法を紹介するイベントです。
https://tech-lab.connpass.com/event/311864/

ども!今回はnest.jsでFirebase Admin SDKを取りまわす設定と基本的なTODOリストのテストを行いました。Firebase Admin SDKとFirebase SDKの二つあると知らなくて、結構な時間のロスをしましたが、サーバーでFirestoreを扱う場合は、Firebase Admin SDK一択なようなので入門していきます。

最初のお話

ども!ブログ生産が好調な龍ちゃんです。カウントしてみたら、新年が始まって18本のブログを執筆していました。入門記事ばかりなので、あんまりはねないのがコンプレックスなんですけど、出さないよりはいいなと思っています。

さて、今回は「nest.jsでFirebase Admin SDKの初期設定」についてまとめていこうと思います。個人開発と社内アプリなどの小さいアプリケーションでよくFirebaseとデータベースとしてFirestoreを使用しています。基本は、フロントエンドで処理を完結させています。

新米が爆速でMVP開発してみた

そのため、バックエンド言語でFirestoreを扱ったことはないので取り組んでいきます。背景としては、こちらの記事でBFFを構成するにあたって、フロントエンドの処理をバックエンドに移譲するために実装していきます。

シンプルにBFFに入門してみる

今回の記事でわかる内容としては、以下になります。

  • Firebase SDKとFirebase Admin SDKの二つの選択肢がある
  • nest.jsでFirebase Admin SDKを扱うための設定
  • nest.jsでFirestoreに対してTODOリストのCRUD処理:DataConverter付

それでは始めましょう。

Firebase SDKとFirebase Admin SDKの二つ違いがある

こちらからが本題になります。NodeJSでFirestoreを扱う場合、2つの選択肢があります。フロントエンドの場合は、Firebase SDK一択になります。以下にイメージを置いておきます。

FirebaseSDKのベン図

Firebase(SDK)でやれることはFirebase Admin(SDK)で実現可能です。逆はできません。つまりFirebse(SDK)は機能制限されていると考えたほうが良いです。ただ、データの追加や削除といったものはできますし、フロントエンドのGoogle認証はこちらを使用することで楽に実装することができます。以下に公式のリンクを貼っておきます。

この記事では、Firebase Admin(SDK)を重点的に取り扱います。もし、Firebase(SDK)のCRUD処理を求めている方は、以下の記事を参考にしてみてください。

ReactでFirestore入門

 

nest.jsでFirebase Admin SDKの入門

事前準備として、以下のステップがあります。

  • Firebaseのアプリケーション作成
  • Firebaseサービスアカウントの取得
  • ライブラリのインストール
  • .envファイルにサービスアカウント情報を転記

ライブラリは以下になります。インストールしましょう。

npm install @nestjs/config firebase-admin --save

Firebaseのアプリケーションは作成済みの前提で進めていきます。サービスアカウントの情報は、以下の画像を参考に取得してください。

Firebaseサービスアカウント取得はこちら

取得した情報は以下の形式で、.envに保存してください。サービスアカウントの情報は、たくさんありまが、Firebase Admin SDKの初期化に必要な情報としては、以下の三つの身になります。

FIREBASE_PROJECT_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxx"
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\\nxxxxxxxxxx\\n-----END PRIVATE KEY-----\\n"
FIREBASE_CLIENT_EMAIL="xxxxxxxxxxxxxxxxxxxxxxxxx"

以上で事前準備は終了です。

今回作成するアプリケーションのディレクトリ構造は以下になります。

.
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── config
│   ├── enviroments.module.ts
│   └── enviroments.service.ts
├── todo
│   ├── dto
│   │   ├── request-todo.dto.ts
│   │   └── response-todo.dto.ts
│   ├── todo.controller.spec.ts
│   ├── todo.controller.ts
│   ├── todo.module.ts
│   ├── todo.service.spec.ts
│   └── todo.service.ts
├── main.ts
└── types
    └── todoType.ts

それでは実際の構築に入ります。

Firesbase Admin SDKを扱う初期設定

ここでは、Firebase Admin SDKの初期化を環境変数的に共通化させる処理を書いておきます。config配下のenviroments.module.tsに以下の内容をコピペしてください。

import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EnvironmentsService } from './enviroments.service';

@Global()
@Module({
  imports: [ConfigModule.forRoot({ envFilePath: ['.env', '.env.local'] })],
  providers: [EnvironmentsService],
  exports: [EnvironmentsService],
})
export class EnvironmentsModule {}

同じくenviroments.service.tsに以下の内容をコピペしてください。

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import { credential } from 'firebase-admin';
import { App, initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

@Injectable()
export class EnvironmentsService {
  constructor(private configService: ConfigService) {}

  private firebaseApp: App;

  get firebaseAppInstance() {
    if (this.firebaseApp) return this.firebaseApp;

    this.firebaseApp = initializeApp({
      credential: credential.cert({
        projectId: this.configService.get('FIREBASE_PROJECT_ID'),
        privateKey: this.configService
          .get('FIREBASE_PRIVATE_KEY')
          .split(String.raw`\\n`)
          .join('\\n'),
        clientEmail: this.configService.get('FIREBASE_CLIENT_EMAIL'),
      }),
    });
    return this.firebaseApp;
  }

  get firestoreDB() {
    const DB = getFirestore(this.firebaseAppInstance);
    return DB;
  }
}

firebaseAppInstance() でfirebase Admin SDKの初期化を行っています。それを用いてFirestoreアクセス用にfirestoreDB() を呼び起こしています。enviromentsはグローバルインポートしているので、インポートさえすればFirestoreに楽にアクセスすることができるようになります。

以上で初期化の共通化は終了になります。

nest.jsのFirebase Admin SDKでFirestore CRUD処理

まずは公式リファレンスです。

ここでは、CRUD処理周りを確認していきたいと思います。今回は、TODOリストを例にして出していきます。型定義としては、以下になります。uidはFirestore側のドキュメントIDを保持すると仮定します。

export class todoType {
  uid: string;        // ドキュメントID
  userID: string;
  text: string;
  timestamp: Date;
  done: boolean;
}

先にコードを出します。todo.service.tsになります。

import { Injectable } from '@nestjs/common';

import { DocumentData, FieldValue, FirestoreDataConverter, QueryDocumentSnapshot } from 'firebase-admin/firestore';
import { EnvironmentsService } from 'src/config/enviroments.service';
import type { todoType } from '../types/todoType';

@Injectable()
export class LineService {
  constructor(private readonly env: EnvironmentsService) {}
  todoDB = this.env.firestoreDB.collection('todo');

  todoConverter: FirestoreDataConverter<todoType> = {
    fromFirestore(snapshot: QueryDocumentSnapshot<DocumentData>): todoType {
      return {
        uid: snapshot.id,
        userID: snapshot.get('userID'),
        text: snapshot.get('text'),
        done: snapshot.get('done'),
        timestamp: snapshot.get('timestamp'),
      };
    },
    toFirestore(todo: todoType): DocumentData {
      return {
        userID: todo.userID,
        text: todo.text,
        done: todo.done,
        timestamp: todo.timestamp,
      };
    },
  };

  createTodo = async (todo: Omit<todoType, 'uid' | 'timestamp'>): Promise<void> => {
    const collRef = this.todoDB.withConverter(this.todoConverter);
    await collRef.add({ uid: '', ...todo, timestamp: FieldValue.serverTimestamp() });
  };

  readTodo = async (): Promise<todoType[]> => {
    const collRef = this.todoDB.withConverter(this.todoConverter);
    const snapshot = await collRef.get();
    const result = snapshot.docs.map((doc) => doc.data());
    return result;
  };

  updateTodo = async (todo: todoType): Promise<void> => {
    const collRef = this.todoDB;
    const docRef = collRef.doc(todo.uid).withConverter(this.todoConverter);
    await docRef.update({ ...todo });
  };

  deleteTodo = async (uid: string): Promise<void> => {
    const collRef = this.todoDB;
    const docRef = collRef.doc(uid);
    await docRef.delete();
  };
}

順に解説していきます。

入出力時データ加工:FirestoreDataConverter

こちらでは、Firestoreの入出力に型定義をはめ込むためにFirestoreDataConverterを使用しています。

todoConverter: FirestoreDataConverter<todoType> = {
  fromFirestore(snapshot: QueryDocumentSnapshot<DocumentData>): todoType {
    return {
      uid: snapshot.id,
      userID: snapshot.get('userID'),
      text: snapshot.get('text'),
      done: snapshot.get('done'),
      timestamp: snapshot.get('timestamp'),
    };
  },
  toFirestore(todo: todoType): DocumentData {
    return {
      userID: todo.userID,
      text: todo.text,
      done: todo.done,
      timestamp: todo.timestamp,
    };
  },
};

英単語そのままですが、fromFirestoreがFirestoreから情報を取得する場合に処理を挟みます。ドキュメントのIDを固有のIDとしてデータ成型しています。toFirestoreでは、書き込み処理を実装しています。todoTypeにはuidが存在しますが、こちらはドキュメントIDになるので情報としては保持しなくて問題ありません。このように入出力に処理を挟むことができます。

データ作成:createTodo

こちらでは、データの追加を行っています。

createTodo = async (todo: Omit<todoType, 'uid' | 'timestamp'>): Promise<void> => {
  const collRef = this.todoDB.withConverter(this.todoConverter);
  await collRef.add({ uid: '', ...todo, timestamp: FieldValue.serverTimestamp() });
};

データ書き込みのタイミングで、ドキュメントのIDとタイムスタンプは払い出されるので型では除外しています。また、uidでは空文字を入れ、timestampではFirebaseのサーバー情報をそのまま入力しています。

データ呼び出し: readTodo

こちらでは、すべてのデータを取得しています。

readTodo = async (): Promise<todoType[]> => {
  const collRef = this.todoDB.withConverter(this.todoConverter);
  const snapshot = await collRef.get();
  const result = snapshot.docs.map((doc) => doc.data());
  return result;
};

コレクションをすべて取得して、その中のドキュメントを取得しています。FirestoreDataConverterを挟んでいることで、ドキュメントの型はtodoTypeになっています。

データの更新:updateTodo

ここでは、特定のデータの更新を行っています。

updateTodo = async (todo: todoType): Promise<void> => {
  const collRef = this.todoDB;
  const docRef = collRef.doc(todo.uid).withConverter(this.todoConverter);
  await docRef.update({ ...todo });
};

取得したドキュメントのIDで検索して、内容を更新しています。値を渡す際に展開して渡す必要があります。

データの削除: deleteTodo

ここでは、特定のデータの削除を行っています。

deleteTodo = async (uid: string): Promise<void> => {
  const collRef = this.todoDB;
  const docRef = collRef.doc(uid);
  await docRef.delete();
};

検索して削除というシンプルなコードになります。

おわりもす

お疲れ様です。Firestoreを取り扱うSDKが二つあることを発見してからは、サクサクと検証することができました。Firebase SDKとFirebase Admin SDKで、Firestoreに対するデータの呼び出し方法が異なるのは、びっくりしました。Firebase Admin SDKのほうが手続き的に記載するので、nest.jsの思想とマッチしているように感じています。

バッチ処理やトランザクション周りを組んだことがないので、まだまだ勉強することが多そうです。

これで以下のBFFを組むことができると思います。

改良版MVP構成図

新しく個人プロジェクトも始めたので、ブログのネタが尽きるよりも早くに仕上げないといけませんね。

ではでは、また次のブログで!

Twitterのほうもよろしくお願いします。

アバター画像
About 龍:Ryu 106 Articles
2022年入社で主にフロントエンドの業務でTailwindと遊ぶ日々。お酒とうまいご飯が好きで、運動がちょっと嫌いなエンジニアです。しゃべれるエンジニアを目指しておしゃべりとブログ執筆に注力中(業務もね)//
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる