Firebase FunctionsにDIを導入しようと思ったきっかけ
最近、私がサーバーサイドを開発するとき、Kotlinを選択することが増えました。
以前、私はTypeScriptを使って、NestJSやExpressを駆使しながらアプリケーションを開発していました。
しかし、Android開発や、Kotlinの素晴らしさに触れたことで、KotlinとKtorフレームワークを使って開発することに喜びを感じるようになりました。
Ktorを使うとき、Koinライブラリを使ってDIを行います。クリーンアーキテクチャを意識しながら開発を進めていきます。このスタイルに慣れるにつれ、DIを使うことはコード管理のしやすさに繋がると、改めて思うようになりました。
サーバーサイドのプロジェクトでは全てKotlinとKtorを使って開発したいです。
ところが、Firebase FunctionsではNode.jsを使った開発が推奨されています。
Firebase CLIでも選択できるのはJavaScriptとTypeScriptだけです。
そのため、公式に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
- impl/
- service/
- di/
- AuthenticationModule.ts
- FirebaseModule.ts
- UserModule.ts
- domain/
- model/
- UserRecord.ts
- User.ts
- repository
- AuthenticationRepository.ts
- UserRepository.ts
- model/
- infrastructure/
- firebase/
- impl/
- AccountServiceImpl.ts
- UserStorageServiceImpl.ts
- AccountService.ts
- UserStorageService.ts
- impl/
- model/
- UserRecord.ts
- User.ts
- repository
- AuthenticationRepositoryImpl.ts
- UserRepositoryImpl.ts
- firebase/
- application/
呼び出し可能関数が依存するクラス
呼び出し可能関数が直接に依存するクラスは、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
-
AuthenticationServiceImpl
→AuthenticationRepository
-
AuthenticationRepositoryImpl
→AccountService
-
AccountServiceImpl
→Auth
呼び出し可能関数は、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
-
UserServiceImpl
→UserRepository
-
UserepositoryImpl
→UserStorageService
-
UserStorageServiceImpl
→Auth
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以外のクラスからは感知されない存在となります。
ここでは、Auth
やFirestore
のクラスを使いながら、データに関する操作を実際に行います。
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
を使用していることに疑問を持ちましたか?
呼び出し可能関数の中で処理を行うときには、createdAt
はDate
になっています。
その変換は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
}
}
}
このアプリでは、uid
とemail
のみしか使いません。
UserRecord
では、uid
とemail
のみを元の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
では、Infrastructure
のUser
を受け取っています。
ご覧の通り、Timestamp
をDate
に変換しています。Date
の方がTimestamp
に比べて、より使いやすいです。
domain/repository/AuthenticationRepository.ts
updateEmail()
の返り値に注目してください。Promise<boolean>
となっています。
AccountService
のupdateEmail()
と同じ名前ですが、返り値の型は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
コントラクタインジェクションで、AuthRepository
とUserRepository
を取得しています。
このように、複数のRepositoryを組み合わせることができます。
呼び出し可能関数では、UserService
のupdateEmail()
を一度だけ呼び出します。内部では複数の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;
}
}
呼び出し可能関数の実装
AuthenticationService
とUserService
を使って解決を行っています。その結果、authService
とuserService
に値をDIしています。
AuthenticationService
のverifyIdToken()
を呼び出してIDトークンの有効性を検証します。
UserService
のupdateName()
を呼び出して名前の変更を行います。
返り値として、変更後の値を返します。
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を導入したい開発者の方へのヒント集として、ご一読していただけたら幸いです。