0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Firebase】TSyringeを使ってFirebase FunctionsにDIを導入した時のまとめ

Posted at

Firebase FunctionsにDIを導入しようと思ったきっかけ

最近、私がサーバーサイドを開発するとき、Kotlinを選択することが増えました。
以前、私はTypeScriptを使って、NestJSやExpressを駆使しながらアプリケーションを開発していました。
しかし、Android開発や、Kotlinの素晴らしさに触れたことで、KotlinとKtorフレームワークを使って開発することに喜びを感じるようになりました。

Ktorを使うとき、Koinライブラリを使ってDIを行います。クリーンアーキテクチャを意識しながら開発を進めていきます。このスタイルに慣れるにつれ、DIを使うことはコード管理のしやすさに繋がると、改めて思うようになりました。

サーバーサイドのプロジェクトでは全てKotlinとKtorを使って開発したいです。

ところが、Firebase FunctionsではNode.jsを使った開発が推奨されています。
Firebase CLIでも選択できるのはJavaScriptTypeScriptだけです。

そのため、公式にKotlinが採用されるまで、Node.jsを使うことに決めました。

Firebase Functionsでの開発でもKoinのようなDIを行いたい。

まずは、現在開発中のプロジェクトからリファクタリングに取り組むことに決めました。

そして、機能の一部のリファクタリングに成功しました。

どのようにディレクトリ分けを行い、どのように機能ごとにモジュール分割をすることで、Firebase FunctionsにDIの導入を行えるかを忘備録として、自分のためにまとめます。

DIに必要なパッケージの追加

DI関連のパッケージとしては、Inversifyが有名です。

しかし、導入のしやすさや、使い方のシンプルさを考慮した結果、microsoftが開発しているTSyringeを採用しています。

README.mdに、TSyringeの使い方が記載されています。最初にご一読されることを強く推奨します。

もし、ドキュメントの内容が難しいと思う方には、こちらの記事がおすすめです。

私は、TSyringeの使い方の大部分を、クラスメソッドさんのこの記事から学ばせていただきました。ぜひお読みください。

yarn add tsyringe reflect-metadata

Reflect API用のポリフィルを追加します。そのため、reflect-metadataをインストールしています。

TSyringeを正常に動作させるために、次の設定をtsconfig.jsonに追加します。

...
"compilerOptions": {
    ...
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
}
...

containerを使う前に、必ず次のインポート文を追加します。

これは、Firebase Functionsの解説です。src/index.tsの先頭に次の行を追加します。

import "reflect-metadata";

TSyringeを使う準備が整いました。

次の章から、具体的な実装方法について、解説していきます。

ファイル構造

DIを行うのに必要なファイルを作成します。
太字になっている箇所が実際に作成しないといけないTypeScriptファイルです。

  • src/
    • application/
      • service/
        • impl/
          • AuthenticationServiceImpl.ts
          • UserServiceImpl.ts
        • AuthenticationService.ts
        • UserService.ts
    • di/
      • AuthenticationModule.ts
      • FirebaseModule.ts
      • UserModule.ts
    • domain/
      • model/
        • UserRecord.ts
        • User.ts
      • repository
        • AuthenticationRepository.ts
        • UserRepository.ts
    • infrastructure/
      • firebase/
        • impl/
          • AccountServiceImpl.ts
          • UserStorageServiceImpl.ts
        • AccountService.ts
        • UserStorageService.ts
      • model/
        • UserRecord.ts
        • User.ts
      • repository
        • AuthenticationRepositoryImpl.ts
        • UserRepositoryImpl.ts

呼び出し可能関数が依存するクラス

呼び出し可能関数が直接に依存するクラスは、application/service/ 配下のクラスです。
infrastructure/domain/ に存在するクラスにはアクセスしません。

diディレクトリ内のファイルの役割

containerを通してクラスを実際に登録するための処理を、diディレクトリ内のファイル内で行なっています。

KtorでKoinを使ったことがある場合は、次のような書き方に見覚えがある方も多いかもしれません。

val authenticationModule = module {
    singleOf(::AuthenticationServiceImpl) {
        bind<AuthenticationService>()
        createdAtStart()
    }
}

TSyringeでは、diディレクトリ配下に保存されたファイルが似たような処理を行なっています。

di/FirebaseModule.ts

FirebaseModule.tsにはとても大事なfirebaseModule()が定義されています。

"Auth"と"Firestore"というトークンに対して、AuthクラスのインスタンスとFirestoreクラスのインスタンスがDIコンテナに登録されています。

import { container } from "tsyringe"
import admin from "firebase-admin"

export function firebaseModule() {
  container.register(
    "Auth",
    {
      useValue: admin.auth()
    }
  )
  container.register(
    "Firestore",
    {
      useValue: admin.firestore()
    }
  )
}

@inject('Auth')をコンストラクタ内で呼び出すことによって、Authクラスのインスタンスを取得できるようになります。

private auth: Auth | undefined = undefined;

constructor(@inject('Auth') auth: Auth) {
    this.auth = auth;
}

代わりに、コンストラクタインジェクションを行うときは、private readonlyを指定します。

constructor(@inject('Auth') private readonly auth: Auth) {}

コンストラタインジェクションの場合は、別個でプロパティを定義しなくても良いので、とてもスリムなクラスを実装することができます。

di/AuthenticationModule.ts

次の順番で、各クラスや呼び出し可能関数は依存し合っています。

  • 呼び出し可能関数→AuthenticationService
  • AuthenticationServiceImplAuthenticationRepository
  • AuthenticationRepositoryImplAccountService
  • AccountServiceImplAuth

呼び出し可能関数は、AuthenticationServiceインターフェースの実態がどうなっているのかを知りません。呼び出しかの関数が期待しているものはとてもシンプルです。

それは、AuthenticationServiceインターフェースに記載されたメソッドに、求められている引数を渡して、定義されている返り値の型の値が返ってくることだけです。

そして、このファイルでは、そのインターフェースの実装クラスをDIコンテナに登録しています。

各インターフェースの詳細、そして実装クラスの詳細については後ほど解説します。

import { container } from "tsyringe";

import type { AuthenticationService } from "~/application/service/AuthenticationService";
import { AuthenticationServiceImpl } from "~/application/service/impl/AuthenticationServiceImpl";
import type { AuthenticationRepository } from "~/domain/repository/AuthenticationRepository";
import type { AccountService } from "~/infrastructure/database/firebase/AccountService";
import { AccountServiceImpl } from "~/infrastructure/database/firebase/impl/AccountServiceImpl";
import { AuthenticationRepositoryImpl } from "~/infrastructure/database/repository/AuthenticationRepositoryImpl";

export function authenticationModule() {
  container.register<AuthenticationService>(
    "AuthenticationService",
    {
      useClass: AuthenticationServiceImpl
    }
  )
  container.register<AuthenticationRepository>(
    "AuthenticationRepository",
    {
      useClass: AuthenticationRepositoryImpl
    }
  )
  container.register<AccountService>(
    "AccountService",
    {
      useClass: AccountServiceImpl
    }
  )
}

実際の使用例

firebaseModule()は、index.tsで呼び出します。

...

if (admin.apps.length === 0) {
  admin.initializeApp();
}

...

firebaseModule()

...

export const examplePubSub = functions
  .region(REGION)
  .pubsub.topic('example-pubsub')
  .onPublish(...);

この呼び出しによって、Firebase SDKから提供されているクラスのインスタンスを取得することができるようになりました。

具体的な注入方法は、後ほど説明します。

di/UserModule.ts

この関数では、主にFirestoreのusersドキュメントに関するクラスをDIコンテナに登録します。

次の順番で、各クラスや呼び出し可能関数は依存し合っています。

  • 呼び出し可能関数→UserService
  • UserServiceImplUserRepository
  • UserepositoryImplUserStorageService
  • UserStorageServiceImplAuth
import { container } from 'tsyringe';

import { UserServiceImpl } from '~/application/service/impl/UserServiceImpl';
import type { UserService } from '~/application/service/UserService';
import type { UserRepository } from '~/domain/repository/UserRepository';
import { UserStorageServiceImpl } from '~/infrastructure/database/firebase/impl/UserStorageServiceImpl';
import type { UserStorageService } from '~/infrastructure/database/firebase/UserStorageService';
import { UserRepositoryImpl } from '~/infrastructure/database/repository/UserRepositoryImpl';

export function userServiceModule() {
  container.register<UserService>('UserService', {
    useClass: UserServiceImpl,
  });
  container.register<UserRepository>('UserRepository', {
    useClass: UserRepositoryImpl,
  });
  container.register<UserStorageService>('UserStorageService', {
    useClass: UserStorageServiceImpl,
  });
}

実際の使用例

index.tsで、userServiceModule()を呼び出します。

...

if (admin.apps.length === 0) {
  admin.initializeApp();
}

...

firebaseModule()
userServiceModule()

...

export const examplePubSub = functions
  .region(REGION)
  .pubsub.topic('example-pubsub')
  .onPublish(...);

必ず、firebaseModule()を一番最初に呼び出してください。userServiceModule()内で登録した実装クラスの中に、firebaseModule()で登録したトークンに依存しているクラスがあるためです。

依存している順番通りに、DIコンテナにクラスを登録してください。

Infrastructure

Ktorプロジェクトでは、DAOに相当する領域となります。そのためRepository以外のクラスからは感知されない存在となります。

ここでは、AuthFirestoreのクラスを使いながら、データに関する操作を実際に行います。

infrastructure/database/firebase/AccountService.ts

Authを使ってFirebase Authenticationに対して処理を行うメソッドの型を定義します。
DIされるクラスからは、このインターフェースしか見えないため、実際にAuthが使われているかどうかは知りません。

ここでは、IDトークンが有効かを判断するverifyIdToken()と、Firebase Authenticationに登録されているユーザーのメールアドレスを変更するupdateEmail()を公開しています。

import type { UserRecord } from "firebase-functions/v1/auth";

export interface AccountService {
  verifyIdToken: (idToken: string) => Promise<boolean>;
  updateEmail: (uid: string, email: string) => Promise<UserRecord | undefined>;
}

infrastructure/database/firebase/AccountServiceImpl.ts

DIコンテナで登録するAccountServiceの実態クラスを定義します。

RepositoryでAccountServiceを解決するときに、このクラスのインスタンスが注入されます。

import type { auth } from 'firebase-admin';
import type { UserRecord } from 'firebase-functions/v1/auth';
import { inject, injectable } from 'tsyringe';

...

@injectable()
export class AccountServiceImpl implements AccountService {
  constructor(@inject('Auth') private readonly auth: auth.Auth) {
    this.auth = auth;
  }

  async verifyIdToken(idToken: string): Promise<boolean> {
    const result = await this.auth?.verifyIdToken(idToken);
    if (result) {
      return true;
    }
    return false;
  }

  async updateEmail(uid: string, email: string): Promise<UserRecord | undefined> {
    try {
      const userRecord = await this.auth?.updateUser(uid, {
        email: email,
        emailVerified: false,
      });
      return userRecord;
    } catch (e: unknown) {
      return undefined;
    }
  }
}

infrastructure/database/model/User.ts

UserStorageServiceを説明する前に、Firestoreドキュメントと1:1で対応するモデルの説明を行います。

infrastructure/database/model/配下に存在するインターフェースは、Firestoreのドキュメントを表します。

ここでは、usersコレクションのドキュメントを表すインターフェースを作成しています。

import type { firestore } from "firebase-admin";

export interface User {
  email: string;
  name: string;
  createdAt?: firestore.Timestamp;
}

createdAtプロパティに対して、Dateの代わりにfirestore.Timestampを使用していることに疑問を持ちましたか?

呼び出し可能関数の中で処理を行うときには、createdAtDateになっています。

その変換はRepositoryで行われます。

InfrastructureではFirestoreの生の値を表現したいので、ドキュメントと同じfirestore.Timestampを使っています。

このインターフェースは、次のUserStorageService.tsから参照されます。

infrastructure/database/firebase/UserStorageService.ts

UserStorageServiceはRepositoryから参照されます。

usersドキュメントのemailフィールドを更新するupdateEmail()と、nameフィールドを更新するupdateName()を公開しています。

export interface UserStorageService {
  updateEmail: (uid: string, email: string) => Promise<User | undefined>;
  updateName: (uid: string, name: string) => Promise<User | undefined>;
}

infrastructure/database/firebase/UserStorageService.ts

コンストラクタインジェクションによって、Firestoreインスタンスを取得しています。

データの更新後、withConverter()を使ってUser型に変換した値を呼び出し元に返しています。

import { inject, injectable } from 'tsyringe';
import type { firestore } from 'firebase-admin';
import type { User } from '~/infrastructure/database/model/User';
import type { UserStorageService } from '~/infrastructure/database/firebase/UserStorageService'

...

@injectable()
export class UserStorageServiceImpl implements UserStorageService {
  constructor(@inject('Firestore') private readonly firestore: firestore.Firestore) {}

  async updateEmail(uid: string, email: string): Promise<User | undefined> {
    if (!this.firestore) {
      return undefined;
    }
    const query = this.firestore.collection('users').doc(uid);
    await query.update({
      email: email,
      isVerified: false,
    });
    const fetchQuery = this.firestore.collection('users').doc(uid).withConverter(converter<User>());
    const snapshot = await fetchQuery.get();
    const data = snapshot.data();
    return data;
  }

  async updateName(uid: string, name: string): Promise<User | undefined> {
    if (!this.firestore) {
      return undefined;
    }
    const query = this.firestore.collection('users').doc(uid);
    await query.update({
      name: name,
    });
    const fetchQuery = this.firestore.collection('users').doc(uid).withConverter(converter<User>());
    const snapshot = await fetchQuery.get();
    const data = snapshot.data();
    return data;
  }
}

UserStorageServiceImplは、Repositoryに注入されます。Repositoryでは、UserStorageServiceに対して解決を行うためです。そして、UserStorageServiceに対して、UserStorageServiceImplがDIコンテナに登録されています。

これらの解決は、TSyringeが自動的に行ってくれます。

Repository

RepositoryはFirebase AuthやFirestoreの操作のロジックを抽象化する役割として働きます。

Interfaceを通してRepositoryの実装を行なっているため、Firebaseに関するRepositoryの実装をinfrastructureディレクトリのファイルに閉じ込めることことができます。本来なら外側の層にあるInfrastructureの層には、その内側にあるServiceなどの層からアクセスすることはできないのですが、この実装のおかげで、Repositoryインターフェースのメソッドを呼び出しているService層などからはInfrastructure層を意識することなくDomain層のRepositoryのみを意識した処理を行います。

domain/model/UserRecord.ts

インターフェースや、その実装クラスを説明する前に、モデルを定義します。
モデルは、呼び出し可能関数から見えるドキュメントです。前に説明したモデルとは全く異なるものです。モデルはより抽象度を増しています。呼び出し可能関数にとって、とても扱いやすい形になっています。

AuthenticationServiceで使われているUserRecordは、Firebase SDKから提供されています。
Firebase SDKのUserRecordのプロパティには、このアプリケーションでは使わないプロパティやメソッドなどが多くあります。

呼び出し可能関数にとって、使いやすいUserRecordを定義します。

import type { UserRecord as FirebaseUserRecord } from 'firebase-functions/v1/auth';

export class UserRecord {
  uid: string = "";
  email?: string = "";

  constructor(_userRecord?: FirebaseUserRecord) {
    if (_userRecord) {
      this.uid = _userRecord.uid
      this.email = _userRecord.email
    }
  }
}

このアプリでは、uidemailのみしか使いません。
UserRecordでは、uidemailのみを元のUserRecordから公開しています。

domain/model/User.ts

Userは、Repositoryより上の層から参照されます。

import type { User as UserModel } from "~/infrastructure/database/model/User"

export class User {
  createdAt?: Date;
  email: string = "";
  name: string = "";

  constructor(_user?: UserModel) {
      if (_user) {
          this.createdAt = _user.createdAt ? new Date(_user.createdAt.seconds * 1000) : undefined,
          this.email = _user.email,
          this.name = _user.name,
      }
  }
}

constructorでは、InfrastructureUserを受け取っています。
ご覧の通り、TimestampDateに変換しています。Dateの方がTimestampに比べて、より使いやすいです。

domain/repository/AuthenticationRepository.ts

updateEmail()の返り値に注目してください。Promise<boolean>となっています。
AccountServiceupdateEmail()と同じ名前ですが、返り値の型はPromise<UserRecord | undefined>です。

Repositoryはデータに対する操作の抽象度を上げています。Repositoryより上の層は難しいことを意識せずに使えます。このアプリにおけるRepositoryより上の層では、変更の成果のみを知りたいです。
Repositoryでは、Promise<boolean>を返すことでその希望に答えています。

import type { UserRecord } from "../model/UserRecord"

export interface AuthenticationRepository {
  verifyIdToken: (idToken: string) => Promise<boolean>;
  updateEmail: (uid: string, email: string) => Promise<boolean>;
}

infrastructure/database/repository/AuthenticationRepositoryImpl.ts

コンストラクタインジェクションによって、AccountServiceインスタンスを取得しています。
このクラスは、infrastructure/database/repository/配下に作成されています。
Repositoryは、データベースへのアクセスを隠蔽する目的を持っていることを思い出してください。

Firebaseにアクセスするという実処理とインターフェースの部分を切り離したいです。そのため、domain/配下にこのクラスは作成しません。

import { inject, injectable } from 'tsyringe';
import { UserRecord } from '~/domain/model/UserRecord';
import type { UserStorageService } from '~/domain/repository/AuthenticationRepository';
import type { AccountService } from '~/infrastructure/database/firebase/AccountService';
...

@injectable()
export class AuthenticationRepositoryImpl implements AuthenticationRepository {

  constructor(@inject('AccountService') private readonly accountService: AccountService) {}

  async verifyIdToken(idToken: string): Promise<boolean> {
    try {
      const result = await this.accountService?.verifyIdToken(idToken);
      if (result) {
        return true;
      }
      return false
    } catch (error: unknown) {
      return false
    }
  }

  async updateEmail(uid: string, email: string): Promise<boolean> {
    try {
      const result = await this.accountService?.updateEmail(uid, email);
      if (result) {
        return true;
      }
      return false;
    } catch (error: unknown) {
      return false;
    }
  }
}

domain/repository/UserRepository.ts

先ほど定義した、Userを返すメソッドを公開しています。メソッド名はInfrastructureで定義したものと同じですが、返り値の型が違います。このUserにはTimestampのプロパティは存在しません。

import type { User } from '~/domain/model/User';

export interface UserRepository {
  updateEmail: (uid: string, email: string) => Promise<User | undefined>;
  updateName: (uid: string, name: string) => Promise<User | undefined>;
}

infrastructure/database/repository/UserRepositoryImpl.ts

このクラスも同様に、インターフェースと実処理を行うクラスを切り分けています。
Firestoreを直接扱っていません。UserStorageServiceインターフェースを通してやり取りをしています。ご覧の通り、メソッドの内容はとてもシンプルです。

つまりここでも、Firebaseに対する操作は隠蔽されているのです。

import { inject, injectable } from 'tsyringe';
import { User } from '~/domain/model/User';
import type { UserRepository } from '~/domain/repository/UserRepository';
import type { UserStorageService } from '~/infrastructure/database/firebase/UserStorageService';

...

@injectable()
export class UserRepositoryImpl implements UserRepository {
  constructor(@inject('UserStorageService') private readonly userStorageService: UserStorageService) {}

  async updateEmail(uid: string, email: string): Promise<User | undefined> {
    const result = await this.userStorageService?.updateEmail(uid, email);
    if (!result) {
      return undefined;
    }
    const user: User = new User(result);
    return user;
  }

  async updateName(uid: string, name: string): Promise<User | undefined> {
    const result = await this.userStorageService?.updateName(uid, name);
    if (!result) {
      return undefined;
    }
    const user: User = new User(result);
    return user;
  }
}

Service

ServiceはRepositoryでのFirebaseに対する操作の処理などを用いて、ビジネスロジックを実装する役割を担います。

Serviceに所属するクラスは、Repositoryに所属するクラスを使ってビジネスロジックの実装を行います。Serviceに所属するクラスは、呼び出し可能関数とRepositoryの間に入り、処理の橋渡しを行います。

application/service/AuthenticationService.ts

呼び出し可能関数に、ビジネスロジックのコードは書かれません。
具体的な処理を行うときは、Serviceに所属するクラスのメソッドを呼び出します。呼び出し可能関数は、その返り値を、クライアントに返します。

import type { UserRecord } from "~/domain/model/UserRecord";

export interface AuthenticationService {
  verifyIdToken: (idToken: string) => Promise<boolean>;
  updateEmail: (uid: string, email: string) => Promise<boolean>;
}

application/service/impl/AuthenticationServiceImpl.ts

AuthenticationServiceImplは、Repositoryのメソッドを呼び出すだけです。その裏側で、どんな処理が行われているかを知ることはありません。

import { inject, injectable } from "tsyringe";
import type { UserRecord } from "~/domain/model/UserRecord";
import type { AuthenticationService } from "~/application/service/AuthenticationService";
import type { AuthenticationRepository } from "~/domain/repository/AuthenticationRepository";

...

@injectable()
export class AuthenticationServiceImpl implements AuthenticationService {
  constructor(@inject('AuthenticationRepository') private readonly authRepository: AuthenticationRepository) {}

  async verifyIdToken(idToken: string): Promise<boolean> {
    const result = await this.authRepository.verifyIdToken(idToken);
    if (result) {
      return true;
    }
    return false;
  }

  async updateEmail(uid: string, email: string): Promise<boolean> {
    const result = await this.authRepository.updateEmail(uid, email);
    if (result) {
      return true;
    }
    return false;
  }
}

application/service/UserService.ts

呼び出し可能関数は、Userモデルに対して変更を行いたいです。それはusersドキュメントではないのです。実態としてはusersコレクションに存在するドキュメントへの変更です。しかし、呼び出し可能関数にとっては関係のない話です。

呼び出し可能関数は、UserServiceで公開されたメソッドを呼び出すだけです。

import type { User } from "~/domain/model/User";

export interface UserService {
  updateEmail: (uid: string, email: string) => Promise<User | undefined>;
  updateName: (uid: string, name: string) => Promise<User | undefined>;
}

application/service/impl/UserServiceImpl.ts

コントラクタインジェクションで、AuthRepositoryUserRepositoryを取得しています。
このように、複数のRepositoryを組み合わせることができます。

呼び出し可能関数では、UserServiceupdateEmail()を一度だけ呼び出します。内部では複数のRepositoryのメソッドを呼び出しています。何度でも言います。呼び出し可能関数では、一度だけで良いのです。

データベースに対する処理は、何層にも分けることで、呼び出し側から隠蔽することができます。

import { inject, injectable } from 'tsyringe';
import type { User } from '~/domain/model/User';
import type { UserService } from '~/application/service/UserService';
import type { AuthenticationRepository } from '~/domain/repository/AuthenticationRepository';
import type { UserRepository } from '~/domain/repository/UserRepository';
...

@injectable()
export class UserServiceImpl implements UserService {
  constructor(
    @inject('AuthenticationRepository') private readonly authRepository: AuthenticationRepository,
    @inject('UserRepository') private readonly userRepository: UserRepository,
  ) {}

  async updateEmail(uid: string, email: string): Promise<User | undefined> {
    const result1 = await this.authRepository.updateEmail(uid, email);
    if (!result1) {
      return undefined;
    }
    const result2 = await this.userRepository.updateEmail(uid, email);
    return result2;
  }

  async updateName(uid: string, name: string): Promise<User | undefined> {
    const result1 = await this.authRepository.updateDisplayName(uid, name);
    if (!result1) {
      return undefined;
    }
    const result2 = await this.userRepository.updateName(uid, name);
    return result2;
  }
}

呼び出し可能関数の実装

AuthenticationServiceUserServiceを使って解決を行っています。その結果、authServiceuserServiceに値をDIしています。

AuthenticationServiceverifyIdToken()を呼び出してIDトークンの有効性を検証します。

UserServiceupdateName()を呼び出して名前の変更を行います。

返り値として、変更後の値を返します。

import "reflect-metadata";
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

import { authenticationModule } from "./di/AuthenticationModule";
import { firebaseModule } from "./di/FirebaseModule";
import { userServiceModule } from "./di/UserModule";

if (admin.apps.length === 0) {
  admin.initializeApp();
}

firebaseModule()
authenticationModule()
userServiceModule()

const REGION = 'asia-northeast1';

export const updateName = functions.region(REGION).https.onCall(async (
  data,
  context,
): Promise<{ name: string }> => {
  const authService = container.resolve<AuthenticationService>('AuthenticationService');
  const userService = container.resolve<UserService>('UserService');

  // uidの有無を確認する
  const uid = context.auth?.uid;
  if (!uid || uid === '') {
    throw new functions.https.HttpsError(
        'invalid-argument', 
        'uidを取得することができませんでした'
    );
  }
  // idTokenを取得する
  const idToken = data.idToken ?? undefined;
  if (!idToken) {
    throw new functions.https.HttpsError(
        'invalid-argument', 
        'idTokenを取得できませんでした'
    );
  }
  // nameを取得する
  const name = data.name ?? undefined;
  if (!name || name === '') {
    throw new functions.https.HttpsError(
        'invalid-argument', 
        'nameを取得できませんでした'
    );
  }

  // idTokenを検証する
  const verified = await authService.verifyIdToken(idToken);
  if (!verified) {
    throw new functions.https.HttpsError(
      'permission-denied',
      'idTokenが正しいことを検証できませんでした',
    );
  }

  const result = await userService.updateName(uid, name);
  if (!result) {
    throw new functions.https.HttpsError(
        'internal', 
        '名前の更新を行うことができませんでした'
    );
  }

  return {
    name: name,
  };
});

まとめ

呼び出し可能関数の中身をスッキリさせたい。そして、処理を簡単に追えるようにし、可読性を向上させたい。この目標を頭に入れながらコードを書くことで、あなたが書くコードは、より洗練されエレガントなものになるはずです。

この記事で書かれた内容は、完璧なクリーンアーキテクチャと呼べるものではありません。DDDに関する書籍を読み、リファクタリングを繰り返し、現状に照らし合わせてより良い方法を選択した結果です。
注意点として、必ずしもあなたにとってのベターにはなりえません。

TSyringeを使って、Firebase FunctionsにDIを導入したい開発者の方へのヒント集として、ご一読していただけたら幸いです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?