Google Firestore: Cómo crear una clase auxiliar

google firestore

Cómo crear una clase auxiliar de base de datos para Google Firestore con Node.js

Quería compartir una clase auxiliar de base de datos construida para interactuar con Google Firestore dentro de una API de Node.js.

La abstracción de las operaciones de la base de datos puede ser un desafío.


El siguiente ejemplo asume que ya tiene Firestore instalado y conectado.

Configuración inicial sin clase

export const getUserById = async (id: string): Promise<IUser> => {
  const userRef = await firestoreDb.collection("users").doc(id).get();

  if (!userRef.exists) {
    throw new NotFound("no record found");
  }

  return userRef.data() as IUser;
};

export const updateUser = async (user: IUser): Promise<void> => {
  const userRef = await firestoreDb.collection("users").doc(user.id).get();

  if (!userRef.exists) {
    throw new NotFound("no record found");
  }

  await userRef.ref.update({ ...user });
};

export const createUser = async (id: string, user: IUser): Promise<void> => {
  await firestoreDb.collection("users").doc(id).set(user);
};

export const getPostsByUser = async (id: string): Promise<IPost[]> => {
  const query = await firestoreDb.collection("posts").where("userId", "==", id).get();
  return query.docs.map((item) => item.data() as IPost);
};

export const getUserByUsername = async (username: string): Promise<IUser> => {
  const query = firestoreDb.collection("users").where("username", "==", username);
  const doc = await query.limit(1).get();

  if (doc.empty) throw new NotFound("no record found");

  return doc.docs[0].data() as IUser;
};

Si comenzamos con esto como nuestra configuración inicial para administrar usuarios en nuestra base de datos, todo funciona bien.

Idealmente, queremos una clase que admita lo siguiente:

  1. Funciones de base de datos reutilizables para cualquier colección.
  2. Escriba seguridad.
  3. Solo permite utilizar una conexión de base de datos a la vez.

Lo más importante a lo que debemos aspirar es reducir la cantidad de código duplicado necesario para implementar interacciones similares en otras partes de la aplicación.

Proporcionaré la clase de ejemplo completa a continuación, seguida de la versión actualizada de la configuración inicial.


Asistente de clase de base de datos

type collection = "users" | "posts";

const converter = <T>() => ({
  toFirestore: (data: T) => data,
  fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => snap.data() as T,
});

export class DbInstance {
  private static instance: DbInstance;

  db: FirebaseFirestore.Firestore;

  constructor() {
    this.db = getFirestore(
      initializeApp({
        credential: cert(credentials as any),
      })
    );
  }

  static getInstance(): DbInstance {
    if (!DbInstance.instance) {
      DbInstance.instance = new DbInstance();
    }

    return DbInstance.instance;
  }
  
  async getById<T>(collectionName: collection, id: string): Promise<T> {
    const ref = await this.db.collection(collectionName)
    .withConverter(converter<T>())
    .doc(id)
    .get();

    if (!ref.exists || !ref.data()) {
      throw new NotFound("no record found");
    }

    return ref.data()!;
  }
  
  async updateById(collectionName: collection, id: string, payload: any): Promise<void> {
    const userRef = this.db.collection(collectionName).doc(id);
    await userRef.update({ ...payload });
  }

  async create(collectionName: collection, id: string, payload: any): Promise<void> {
    await this.db.collection(collectionName).doc(id).set(payload);
  }
  
  async getItemsBy<T>(collectionName: collection, where: string, value: any): Promise<T[]> {
    const query = await this.db.collection(collectionName)
    .withConverter(converter<T>())
    .where(where, "==", value)
    .get();
    
    return query.docs.map((item) => item.data());
  }

  async getItemBy<T>(collectionName: collection, where: string, value: any): Promise<T> {
    const query = this.db.collection(collectionName)
    .withConverter(converter<T>())
    .where(where, "==", value);
    
    const doc = await query.limit(1).get();

    if (doc.empty || !doc.docs.length || !doc.docs[0].exists) 
      throw new NotFound("no record found");

    return doc.docs[0].data()!;
  }
  
  async deleteById(collectionName: collection, id: string): Promise<void> {
    await this.db.collection(collectionName).doc(id).delete();
  }
}

La clase anterior abstrae todas las interacciones de la base de datos que mi aplicación necesita. Los tipos de unión y genéricos de TypeScript ayudan a mantener las funciones lo más seguras posible.

Esta clase también se adhiere a un patrón de diseño Singleton, lo que significa que solo se usa una conexión de base de datos a la vez.

Si alguna vez es necesario realizar una interacción única con la base de datos, podemos usar la variable db para interactuar con firestore como lo hacíamos antes.


Funciones de bases de datos actualizadas

const dbInstance = DbInstance.getInstance();

export const getAccountById = async (id: string): Promise<IAccount> => {
  return await dbInstance.getById<IAccountDb>("users", id);
};

export const updateAccount = async (id: string, payload: any): Promise<void> => {
  await dbInstance.updateById("users", id, payload);
};

// example custom implementation with the original firestore setup
export const createAccount = async (user: IAccount): Promise<string> => {
  const query = dbInstance.db.collection("users").where("username", "==", user.username);
  const doc = await query.limit(1).get();

  if (!doc.empty) throw new AlreadyExists(
    `an account with username ${user.username} already exists`
  );

  const newDoc = dbInstance.db.collection("users").doc(user.id);
  await newDoc.set(user);

  return user.id;
};

export const getAccountByUsername = async (username: string): Promise<IAccountDb> => {
  return await dbInstance.getItemBy<IAccount>("users", "username", username);
};

export const deleteAccount = async (id: string): Promise<void> => {
  await dbInstance.deleteById("users", id);
};

// examples for implementation with other collections
export const getPosts = async (userId: string): Promise<IPost[]> => {
  return dbInstance.getItemsBy<IPost>("posts", "userId", userId);
};
  

export const getPostByTag = async (tag: string): Promise<IPost> => {
  return dbInstance.getItemBy<IPost>("posts", "tag", tag);
};

Con esta clase en su lugar, puede reducir la cantidad de texto estándar en su aplicación, sin sacrificar la capacidad de habilitar funciones personalizadas.

Recent Post