はじめに
前回記事の続きです。
ディレクトリ構成、リファクタリングを実施しました。
DDDっぽく集約しています。
▪️シリーズ
TypeScript×Firebase(Step1:プロジェクト作成~データ取得まで)
TypeScript×Firebase(Step2:CRUD処理)
TypeScript×Firebase(Step3:ディレクトリ修正、リファクタリング)←いまここ
ソースコード
~/develop/firebase_typescript$ tree src
src
├── domain
│ └── users
│ ├── model.ts
│ └── repository.ts
├── main.ts
└── repository
└── firestore
├── constants
│ └── document.ts
├── index.ts
└── users
├── create.ts
├── delete.ts
├── index.ts
├── read.ts
└── update.ts
7 directories, 10 files
注目してほしいのが、domain
とrepository
のディレクトリです。
domain
はtypeやinterfaceの型を定義。
repository
は、外部接続するための実装。今回はfirestore
のみのため、1つだけディレクトリを切っていますが、この下にその他の外部接続のディレクトリを切れる様にしています。
domainディレクトリ
~/develop/firebase_typescript$ tree src/domain
src/domain
└── users
├── model.ts
└── repository.ts
2 directories, 2 files
domainディレクトリの階層です。
export type IUser = {
name: string;
email: string;
};
→Firestoreのドキュメントの型を定義。
import { IUser } from './model';
export interface IUserRepository {
findById(id: string): Promise<IUser | null>;
getUsers(): Promise<IUser[]>;
createNewUser(user: IUser): Promise<void>;
updateUser(docId: string, user: IUser): Promise<void>;
deleteUser(docId: string): Promise<void>;
}
→Firestoreと疎通時のInterfaceです。
外部接続のInterfaceを定義する事で見通しが良くなり、テストも実施しやすくなります。
repositoryディレクトリ
外部接続するFirestoreの定義を実装。
ここでは、domain層で定義したInterfaceを活用し、オブジェクトや関数を定義します。
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import * as dotenv from 'dotenv';
dotenv.config();
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
};
export const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
→Firebaseの接続情報の定義。
export const USERS_DOCUMENT_NAME = 'users';
→Firestoreのドキュメントを定義します。
現在は接続ドキュメントが1つのため、1つのみ定義しています。
ドキュメントの増加と共に定義を増やしていきます。
import { Firestore } from 'firebase/firestore';
import { IUserRepository } from '../../../domain/users/repository';
import { IUser } from '../../../domain/users/model';
import { findById, getUsers } from './read';
import { createNewUser } from './create';
import { updateUser } from './update';
import { deleteUser } from './delete';
export class UserRepository implements IUserRepository {
constructor(private db: Firestore) {}
async createNewUser(user: IUser): Promise<void> {
return createNewUser(this.db, user);
}
async updateUser(docId: string, user: IUser): Promise<void> {
return updateUser(this.db, docId, user);
}
async deleteUser(docId: string): Promise<void> {
return deleteUser(this.db, docId);
}
async findById(id: string): Promise<IUser | null> {
return findById(this.db, id);
}
async getUsers(): Promise<IUser[]> {
return getUsers(this.db);
}
}
Firestoreの実装クラスです。repository.ts
のIUserRepository
のInterfaceを元に実装しています。
実際の関数をオブジェクト内で記載すると見通しが悪くなるため、外部で定義した実装内容を呼び出しています。
import { Firestore, addDoc, collection } from 'firebase/firestore';
import { USERS_DOCUMENT_NAME } from '../constants/document';
import { IUser } from '../../../domain/users/model';
// 新しいユーザーを作成する
export async function createNewUser(db: Firestore, user: IUser) {
try {
const docRef = await addDoc(collection(db, USERS_DOCUMENT_NAME), user);
console.log('ドキュメント作成成功!ドキュメントID: ', docRef.id);
} catch (e) {
console.error('ドキュメント作成エラー: ', e);
}
}
import { deleteApp } from 'firebase/app';
import {
Firestore,
collection,
doc,
getDoc,
getDocs,
} from 'firebase/firestore';
import { IUser } from '../../../domain/users/model';
import { USERS_DOCUMENT_NAME } from '../constants/document';
// コレクションの全ドキュメントを取得する
export async function getUsers(db: Firestore): Promise<IUser[]> {
const querySnapshot = await getDocs(collection(db, USERS_DOCUMENT_NAME));
return querySnapshot.docs.map((doc) => doc.data()) as IUser[];
}
// コレクション内の対象ドキュメントを取得する
export async function findById(
db: Firestore,
docId: string
): Promise<IUser | null> {
const docRef = doc(db, USERS_DOCUMENT_NAME, docId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.data() as IUser;
} else {
return null;
}
}
import { Firestore, doc, updateDoc } from 'firebase/firestore';
import { USERS_DOCUMENT_NAME } from '../constants/document';
import { IUser } from '../../../domain/users/model';
export async function updateUser(
db: Firestore,
docId: string,
user: IUser
): Promise<void> {
const docRef = doc(db, USERS_DOCUMENT_NAME, docId);
await updateDoc(docRef, user);
}
import { doc, deleteDoc, Firestore } from 'firebase/firestore';
import { USERS_DOCUMENT_NAME } from '../constants/document';
export async function deleteUser(db: Firestore, docId: string) {
const docRef = doc(db, USERS_DOCUMENT_NAME, docId);
try {
await deleteDoc(docRef);
console.log('ドキュメント削除成功!');
} catch (e) {
console.error('ドキュメント削除エラー: ', e);
}
}
上記が実装クラスで利用する関数になります。
main.ts
既に部品としては完了しています。最後に実際に動かした例をmain.tsに記載してます。
import { IUser } from './domain/users/model';
import { db } from './repository/firestore';
import { UserRepository } from './repository/firestore/users';
(async () => {
const userRepository = new UserRepository(db);
const docId = 'sxsUyqyyuVIK4sfVktne';
const user = await userRepository.findById(docId);
console.log(user);
const users = await userRepository.getUsers();
const newUser: IUser = {
name: 'newUser',
email: 'oo@gmail.com',
};
await userRepository.createNewUser(newUser);
})();
~/develop/firebase_typescript$ ts-node src/main.ts
{ name: 'koji', email: 'koji@gmail.com' }
=======users [
{ email: 'oo@gmail.com', name: 'newUser' },
{ name: 'newUser', email: 'oo@gmail.com' },
{ name: 'newUser', email: 'oo@gmail.com' },
{ email: 'koji@gmail.com', name: 'koji' },
{ email: 'oo@gmail.com', name: 'newUser' }
]
ドキュメント作成成功!ドキュメントID: Q5Lrbi99HQ9fnrHDSQwp
まとめ
今回はディレクトリ、interfaceを上手く活用する事でDDDっぽく記載する事ができました。
見通しが良くなり、保守性が高まったので是非試してみてください!!!!