All files / src/services firestore.db.ts

67.96% Statements 157/231
64.28% Branches 9/14
70% Functions 7/10
67.96% Lines 157/231

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 2321x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x         3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x       2x 1x 1x 1x 1x 1x 1x 1x                                     1x 1x 1x 1x 1x 1x 1x 1x                                   1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x       2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x       2x 1x 1x 1x 1x 1x 1x 1x 1x                                     1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x                 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 1x  
import { CollectionReference, DocumentData, FieldValue, Firestore } from "firebase-admin/firestore";
import { logger } from "firebase-functions/v2";
import { whereClause } from "../interfaces";
 
/**
 * Represents a Firebase model for interacting with Firestore collections.
 */
export class FirebaseModel {
    db: Firestore;
    collection: CollectionReference<DocumentData>;
  
    /**
     * Initializes the FirebaseModel class with the Firestore database instance and the document collection name.
     * @param {string} table - The name of the Firestore collection to interact with.
     * @param {Firestore} firestoreDB - The Firestore database instance.
     */
    constructor(table: string, firestoreDB: Firestore) {
      this.db = firestoreDB;
      this.collection = firestoreDB.collection(table);
    }
  
    /**
     * Fetches a single document from the Firestore collection by its ID.
     * @param {string} id - The document ID to fetch.
     * @returns {Promise<DocumentData | boolean>} Resolves to the document data if found, or `false` if the document doesn't exist.
     */
    async find(id: string): Promise<DocumentData | boolean> {
      try {
        const document = await this.collection.doc(id).get();
        if (document.exists) {
          return document.data() as DocumentData;
        } else {
          return false;
        }
      } catch (error) {
        // unable to find data
        logger.log("find: ", error);
        return false;
      }
    }
  
    /**
     * Checks if a document exists in the Firestore collection by its ID.
     * @param {string} id - The document ID to check.
     * @returns {Promise<boolean>} Resolves to `true` if the document exists, or `false` otherwise.
     */
    async dataExists(id: string): Promise<boolean> {
      try {
        return (await this.collection.doc(id).get()).exists;
      } catch (error) {
        logger.log("data exists: ", error);
        return false;
      }
    }
  
    /**
     * Updates an existing document with the given data in the Firestore collection.
     * @param {object} data - A key-value pair representing the data to update.
     * @param {string} id - The document ID to update.
     * @returns {Promise<boolean>} Resolves to `true` if the update was successful, or `false` if an error occurred.
     */
    async update(data: object, id: string): Promise<boolean> {
      try {
        await this.collection.doc(id).update(data);
        return true;
      } catch (error) {
        logger.log("update: ", error);
        return false;
      }
    }
  
    /**
     * Fetches multiple documents from the Firestore collection. If `ids` are provided, it fetches only those documents. Otherwise, it fetches all documents in the collection.
     * @param {string[]} [ids] - An optional array of document IDs to fetch. If not provided, fetches all documents.
     * @returns {Promise<DocumentData[]>} Resolves to an array of documents' data.
     */
    async findAll(ids?: string[]): Promise<DocumentData[]> {
      try {
        if (ids) {
          const results: DocumentData[] = [];
          for (const id of ids) {
            const item = await this.find(id);
            if (item) {
              results.push(item as DocumentData);
            }
          }
          return results;
        } else {
          return (await this.collection.get()).docs;
        }
      } catch (error) {
        logger.log("findAll: ", error);
        return [];
      }
    }
  
    /**
     * Performs a complex query to find documents that match the given conditions (where clauses).
     * @param {object} clause - The where clauses to filter documents.
     * @param {whereClause[]} clause.wh - An array of `whereClause` objects, each containing a field name (`key`), operator (`operator`), and value (`value`) for filtering documents.
     * @returns {Promise<DocumentData[]>} Resolves to an array of documents that match the query.
     */
    async findWhere({ wh }: { wh: whereClause[] }): Promise<DocumentData[]> {
      try {
        wh.forEach((clause) =>
          this.collection.where(clause.key, clause.operator, clause.value)
        );
        const snapshot = await this.collection.get();
  
        if (!snapshot.empty) {
          return snapshot.docs.map((document) => {
            return { ...document.data(), reference: document.id };
          });
        } else {
          return [];
        }
      } catch (error) {
        throw new Error(`findWhere: , ${error}`);
      }
    }
  
    /**
     * Creates a new document or updates an existing document with the provided data in the Firestore collection.
     * @param {object} data - A key-value pair representing the data to save.
     * @param {string | undefined} id - An optional document ID. If not provided, a new document is created. If provided, the document is updated with the given ID.
     * @returns {Promise<string | boolean>} Resolves to the document ID of the created or updated document if successful, or `false` if an error occurred.
     */
    async save(data: object, id?: string | undefined): Promise<string | boolean> {
      delete (data as any).reference;
      try {
        if (id === undefined) {
          const documentRef = await this.collection.add(data);
          return documentRef.id;
        } else {
          await this.collection.doc(id).set(data);
          return id;
        }
      } catch (error) {
        logger.log("save: ", error);
        return false;
      }
    }
  
    /**
     * Deletes a document from the Firestore collection by its ID.
     * @param {string} id - The document ID to delete.
     * @returns {Promise<boolean>} Resolves to `true` if the deletion was successful, or `false` if an error occurred.
     */
    async delete(id: string): Promise<boolean> {
      try {
        await this.collection.doc(id).delete();
        return true;
      } catch (error) {
        logger.log("delete: ", error);
        return false;
      }
    }
  
    /**
     * Atomically adds items to an array field in a document. If the document does not exist, it creates a new document with the array.
     * @param {any[]} data - The array of items to add to the document's array field.
     * @param {string | undefined} id - The optional document ID. If not provided, a new document is created.
     * @returns {Promise<boolean>} Resolves to `true` if the operation was successful, or `false` if an error occurred.
     */
    async addToArray(data: any[], id?: string): Promise<boolean> {
      try {
        if (id) {
          const dataExist = await this.dataExists(id);
          if (dataExist) {
            await this.collection.doc(id).update({
              data: FieldValue.arrayUnion(...data),
            });
          } else {
            await this.collection.doc(id).set({ data });
          }
        } else {
          await this.collection.add({ data });
        }
        return true;
      } catch (error) {
        return false;
      }
    }
  
    /**
     * Performs a backup of the provided data to Firestore. It checks if the data already exists based on the specified `whereKey` and saves or updates the document accordingly.
     * @param {object} params - The backup parameters.
     * @param {string | string[]} params.whereKey - The key or keys to use for querying existing data.
     * @param {object} params.returnData - The data to be backed up.
     * @param {string} params.dbLabel - The label used to find the data in the provided `returnData`.
     * @returns {Promise<boolean | string>} Resolves to `true` if the backup was successful, or `false` if an error occurred. If the data already exists, the document reference is returned.
     */
    async doBackup({
      whereKey,
      returnData,
      dbLabel,
    }: {
      whereKey: string | string[];
      returnData: object;
      dbLabel: string;
    }): Promise<boolean | string> {
      try {
        const where: whereClause[] = [];
        if (typeof whereKey === "string") {
          where.push({
            key: whereKey,
            operator: "==",
            value: (returnData as any)[dbLabel][whereKey],
          });
        } else {
          whereKey.forEach((key) => {
            where.push({
              key,
              operator: "==",
              value: (returnData as any)[dbLabel][key],
            });
          });
        }
        const itemExist = await this.findWhere({ wh: where });
  
        // Backup return value to Firestore
        return await this.save(
          (returnData as any)[dbLabel],
          (itemExist as any).reference
        );
      } catch (error) {
        logger.log("firestore backup error: ", error);
        return false;
      }
    }
  }