classes/database.js

/**
 * This class contains a suite of static functions that can be used to interact with our app's IndexedDB
 * database. The functions allow for storage, retrieval and deletion of bullet, label, daily, monthly,
 * yearly, and style JSON objects.
 *
 * @classdesc
 * @example <caption>Example usage of Database</caption>
 * // this sample code would store a daily object in the database and print a success statement when it completes
 * const id = 'D 20210519';
 * Database.store(id, { notes: 'this is an example' }, function (stored, id) {
 *   if (stored === true) {
 *     console.log('finished storing object with id ' + id);
 *   } else {
 *     console.log('could not store object with id' + id);
 *   }
 * }, id);
 *
 * @property {string} DATABASE_NAME - The name ('BulletJournal') of the IndexedDB database used by our app
 * @property {Object.<string, string>} Stores - Object containing the constant names of the object stores in the database
 */
export class Database {
  /**
   * @static
   * @property {string} DATABASE_NAME - The name ('BulletJournal') of the IndexedDB database used by our app
   */
  static get DATABASE_NAME () {
    return 'BulletJournal';
  }

  /**
   * @static
   * @property {Object.<string, string>} Stores - Object containing the constant names of the object stores in the database
   */
  static get Stores () {
    return {
      DAILY: 'daily',
      MONTHLY: 'monthly',
      YEARLY: 'yearly',
      BULLETS: 'bullets',
      LABELS: 'labels',
      STYLE: 'style'
    };
  }

  // ------------------------- Start Documentation For Callback Functions -------------------------

  /**
   * Callback function that is run after the database is opened and ready for transactions.
   *
   * @callback openCallback
   * @param {Object} databaseObject - The database object if the operation succeeded; null if it failed
   */

  /**
   * Callback for data retrieval. This callback must be specified to have access to the data retrieved from
   * the database request. If the transaction succeeded, the retrieved object will be passed as the parameter
   * to this function. Otherwise, null will be passed as the parameter to this function. If additional
   * arguments (varArgs) are given to the function that uses this callback, those arguments will be passed
   * to this callback function in order after the fetchedData parameter.
   *
   * @callback fetchCallback
   * @param {Object} fetchedData - The data that is retrieved from the object store if the get transaction
   * succeeded; null if it failed
   */

  /**
   * Callback for success/failure functionality after a storage transaction. Only needed if success and failure
   * conditions must be treated appropriately. If the transaction succeeded, true will be passed as the parameter
   * to this function. Otherwise, false will be passed as the parameter to this function. If additional
   * arguments (varArgs) are given to the function that uses this callback, those arguments will be passed
   * to this callback function in order after the transactionResult parameter.
   *
   * @callback storeCallback
   * @param {boolean} transactionResult - True if the storage transaction succeeded; false if it failed
   */

  /**
   * Callback for success/failure functionality after a deletion transaction. Only needed if success and failure
   * conditions must be treated appropriately. If the transaction succeeded, true will be passed as the parameter
   * to this function. Otherwise, false will be passed as the parameter to this function. If additional
   * arguments (varArgs) are given to the function that uses this callback, those arguments will be passed
   * to this callback function in order after the transactionResult parameter.
   *
   * @callback deleteCallback
   * @param {boolean} transactionResult - True if the delete transaction succeeded; false if it failed
   */

  // -------------------------- End Documentation For Callback Functions --------------------------

  // ----------------------------------- Start Helper Functions -----------------------------------

  /**
   * This function opens the IndexedDB database in order to allow for transactions to be run on the
   * database. It makes an asynchronous call to IndexedDB's open function with the constant database
   * name 'BulletJournal' and creates the appropriate object stores if the database doesn't yet exist.
   * It then runs the given callback function with the resulting database object if the operation
   * succeeds, or null if the operation fails. This function is automatically called as a helper function
   * when the fetch, store, and delete functions are called.
   *
   * @static
   * @param {?openCallback} [callback=null] - Callback function that is run after the database open
   * operation completes (if no callback is provided, nothing is run after the transaction is complete)
   */
  static openDatabase (callback = null) {
    // opens database (async)
    const dbOpenRequest = indexedDB.open(this.DATABASE_NAME, 2);

    // called when database is first created and when db updates are needed
    dbOpenRequest.onupgradeneeded = function (event) {
      // adds empty object stores to the database if they don't exist yet
      const db = dbOpenRequest.result;
      const objectStores = db.objectStoreNames;
      Object.values(Database.Stores).forEach(value => {
        if (!objectStores.contains(value)) {
          db.createObjectStore(value);
        }
      });
      console.log('CREATED DATABASE'); // console logging
    };

    // called when database is opened and ready for read/write operations
    dbOpenRequest.onsuccess = function (event) {
      console.log('OPENED DATABASE'); // console logging
      const db = dbOpenRequest.result;
      if (callback != null) {
        callback(db); // callback can use this database for read/write
      }
    };

    // called when database open operation fails (database can't be opened for transactions)
    dbOpenRequest.onerror = function (event) {
      if (callback != null) {
        callback(null); // callback with null signifying database couldn't be opened
      }
    };
  }

  /**
   * This function deletes the entire IndexedDB database and all its contents. It makes an
   * asynchronous call to IndexedDB's deleteDatabase function with the constant database
   * name 'BulletJournal'. If then runs the given callback function with a true/false parameter
   * based on whether or not the open operation succeeded.
   *
   * WARNING: DO NOT CALL UNLESS YOU WISH FOR ALL THE USER'S DATA TO BE DELETED.
   *
   * @static
   * @param {?deleteCallback} [callback=null] - Callback function that is run after the database delete
   * operation completes (if no callback is provided, nothing is run after the transaction is complete)
   */
  static deleteDatabase (callback = null) {
    // start request to delete database
    const dbDeleteRequest = indexedDB.deleteDatabase(this.DATABASE_NAME);

    // called when database is deleted
    dbDeleteRequest.onsuccess = function (event) {
      console.log('DELETED DATABASE'); // console logging
      if (callback != null) {
        callback(true); // callback signifying the database was deleted
      }
    };

    // called when database failed to be deleted
    dbDeleteRequest.onerror = function (event) {
      if (callback != null) {
        callback(false); // callback signifying the database was not deleted
      }
    };
  }

  /**
   * This function parses a given ID and determines what type of object (ex. bullet, daily, monthly, yearly,
   * etc) the ID represents. It returns the object store that stores that type of object. This function is
   * automatically called as a helper function when the fetch, store, and delete functions are called.
   *
   * @static
   * @param {string} id - The unique string ID that was used as the object's key in the database
   * @returns {string} The name of the object store containing the type of object (ex. bullet, daily, monthly,
   * yearly, etc) that the given ID represents
   */
  static getStoreFromID (id) {
    // get the first character of the ID string
    const firstChar = id.charAt(0);

    // use first character to identify the kind of object
    switch (firstChar) {
      case 'D':
        return this.Stores.DAILY; // ID starting with 'D' signifies daily object
      case 'M':
        return this.Stores.MONTHLY; // ID starting with 'M' signifies monthly object
      case 'Y':
        return this.Stores.YEARLY; // ID starting with 'Y' signifies yearly object
      case 'B':
        return this.Stores.BULLETS; // ID starting with 'B' signifies bullet object
      case 'L':
        return this.Stores.LABELS; // ID starting with 'L' signifies label object
      case 'S':
        return this.Stores.STYLE; // ID starting with 'S' signifies style object
    }
  }

  // ------------------------------------ End Helper Functions ------------------------------------

  // --------------------------------- Start Read/Write Functions ---------------------------------

  /**
   * This function retrieves a single JSON object with the given ID in the database. It first uses the format of
   * the given object ID to determine what type of object (ex. bullet, daily, monthly, yearly, etc) is being
   * retrieved. It then makes an asynchronous call to IndexedDB's get function on the appropriate object store
   * to retrieve the desired object, and then passes the retrieved object to the given callback function if the
   * retrieval operation succeeded, or passes null to the given callback if the retrieval operation failed. In
   * addition to the retrieved object, any other given parameters (specified by varArgs) are also passed to the
   * callback function.
   *
   * @static
   * @param {string} id - The unique string ID that was used as the object's key in the database
   * @param {?fetchCallback} [callback=null] - Callback function that is run after the database transaction
   * completes (if no callback is provided, nothing is run after the transaction is complete)
   * @param {...*} varArgs - Additional arguments that are passed into callback function (in order) along with
   * the retrieved object (which is passed as the first argument to the callback)
   */
  static fetch (id, callback = null, ...varArgs) {
    this.openDatabase(function (db) {
      if (db != null) {
        // call helper function to identify what type of object we are looking for, which determines what object store the object should be in
        const storeName = Database.getStoreFromID(id);

        // starts a transaction to create a get request for the object with the given ID
        const transaction = db.transaction([storeName], 'readwrite');
        const store = transaction.objectStore(storeName);
        const getRequest = store.get(id);

        // on success, call the callback function with the retrieved data and the varArgs if any were provided
        getRequest.onsuccess = function (event) {
          if (callback != null) {
            callback(event.target.result, ...varArgs);
          }
        };

        // on error, call the callback function with null and the varArgs if any were provided
        getRequest.onerror = function (event) {
          if (callback != null) {
            callback(null, ...varArgs);
          }
        };
      } else {
        console.log('ERROR: DATABASE COULD NOT BE OPENED');
      }
    });
  }

  /**
   * This function stores a single JSON object in the database using the given ID. It first uses the format of
   * the given object ID to determine what type of object (ex. bullet, daily, monthly, yearly, etc) is being
   * stored. It then makes an asynchronous call to IndexedDB's put function on the appropriate object store
   * to store the given object, and then passes a boolean (true if the operation succeeds or false if the operation
   * fails) to the given callback function. In addition to the boolean value, any other given parameters (specified by
   * varArgs) are also passed to the callback function.
   *
   * @static
   * @param {string} id - The unique string ID that will be used as the object's key in the database
   * @param {Object} dataObject - The JSON object to be stored
   * @param {?storeCallback} [callback=null] - Callback function that is run after the database transaction
   * completes (if no callback is provided, nothing is run after the transaction is complete)
   * @param {...*} varArgs - Additional arguments that are passed into callback function (in order) along with
   * a boolean representing the success/failure result of the transaction (which is passed as the first argument
   * to the callback)
   */
  static store (id, dataObject, callback = null, ...varArgs) {
    this.openDatabase(function (db) {
      if (db != null) {
        // call helper function to identify what type of object we are storing, which determines what object store the object should be stored in
        const storeName = Database.getStoreFromID(id);

        // starts a transaction to create a put request for the object
        const transaction = db.transaction([storeName], 'readwrite');
        const store = transaction.objectStore(storeName);
        const putRequest = store.put(dataObject, id);

        // on success, call the callback function with true and the varArgs if any were provided
        putRequest.onsuccess = function (event) {
          if (callback != null) {
            callback(true, ...varArgs);
          }
        };

        // on error, call the callback function with false and the varArgs if any were provided
        putRequest.onerror = function (event) {
          if (callback != null) {
            callback(false, ...varArgs);
          }
        };
      } else {
        console.log('ERROR: DATABASE COULD NOT BE OPENED');
      }
    });
  }

  /**
   * This function deletes a single JSON object with the given ID from the database. It first uses the format of
   * the given object ID to determine what type of object (ex. bullet, daily, monthly, yearly, etc) is being
   * deleted. It then makes an asynchronous call to IndexedDB's delete function on the appropriate object store
   * to delete the desired object, and then passes a boolean (true if the operation succeeds or false if the operation
   * fails) to the given callback function. In addition to the boolean value, any other given parameters (specified by
   * varArgs) are also passed to the callback function.
   *
   * @static
   * @param {string} id - The unique string ID that was used as the object's key in the database
   * @param {?deleteCallback} [callback=null] - Callback function that is run after the database transaction
   * completes (if no callback is provided, nothing is run after the transaction is complete)
   * @param {...*} varArgs - Additional arguments that are passed into callback function (in order) along with
   * a boolean representing the success/failure result of the transaction (which is passed as the first argument
   * to the callback)
   */
  static delete (id, callback = null, ...varArgs) {
    this.openDatabase(function (db) {
      if (db != null) {
        // call helper function to identify what type of object we are deleting, which determines what object store the object should be in
        const storeName = Database.getStoreFromID(id);

        // starts a transaction to create a get request for the object with the given ID
        const transaction = db.transaction([storeName], 'readwrite');
        const store = transaction.objectStore(storeName);
        const delRequest = store.delete(id);

        // on success, call the callback function with true and the varArgs if any were provided
        delRequest.onsuccess = function (event) {
          if (callback != null) {
            callback(true, ...varArgs);
          }
        };

        // on success, call the callback function with false and the varArgs if any were provided
        delRequest.onerror = function (event) {
          if (callback != null) {
            callback(false, ...varArgs);
          }
        };
      } else {
        console.log('ERROR: DATABASE COULD NOT BE OPENED');
      }
    });
  }

  /**
   * This function retrieves the keys of all the daily JSON objects in the database. The function makes an
   * asynchronous call to IndexedDB's getAllKeys function on the daily object store to retrieve all the keys
   * of the daily objects in the database, and then passes the retrieved object to the given callback function
   * if the retrieval operation succeeded, or passes null to the given callback if the retrieval operation
   * failed. In addition to the retrieved object, any other given parameters (specified by varArgs) are also
   * passed to the callback function.
   *
   * @static
   * @param {?fetchCallback} [callback=null] - Callback function that is run after the database transaction
   * completes (if no callback is provided, nothing is run after the transaction is complete)
   * @param {...*} varArgs - Additional arguments that are passed into callback function (in order) along with
   * the retrieved object (which is passed as the first argument to the callback)
   */
  static getEntryKeys (callback = null, ...varArgs) {
    this.openDatabase(function (db) {
      if (db != null) {
        // starts a transaction to create a get request for the object with the given ID
        const transaction = db.transaction([Database.Stores.DAILY], 'readwrite');
        const store = transaction.objectStore(Database.Stores.DAILY);
        const getKeysRequest = store.getAllKeys();

        // on success, call the callback function with the retrieved list of keys and the varArgs if any were provided
        getKeysRequest.onsuccess = function (event) {
          if (callback != null) {
            callback(event.target.result, ...varArgs);
          }
        };

        // on error, call the callback function with null and the varArgs if any were provided
        getKeysRequest.onerror = function (event) {
          if (callback != null) {
            callback(null, ...varArgs);
          }
        };
      } else {
        console.log('ERROR: DATABASE COULD NOT BE OPENED');
      }
    });
  }

  // ---------------------------------- End Read/Write Functions ----------------------------------
} // end class Database