Source: localDB.js

/**
 * Class for managing a local database using IndexedDB.
 */
class LocalDB {
    /**
     * Creates an instance of LocalDB.
     * @param {string} databaseName - The name of the database.
     * @param {number} [version=1] - The database version.
     */
    constructor(databaseName, version = 1) {
      this.databaseName = databaseName;
      this.version = version;
      this.db = null;
    }
  
    /**
     * Opens a connection to the database and initializes the schema if needed.
     * @param {function(IDBDatabase): void} schemaCallback - Callback to define the database schema.
     * @returns {Promise<IDBDatabase>} Resolves to the database instance.
     */
    async connect(schemaCallback) {
      return new Promise((resolve, reject) => {
        const request = indexedDB.open(this.databaseName, this.version);
  
        request.onupgradeneeded = (event) => {
          const db = event.target.result;
          try {
            schemaCallback(db);
          } catch (error) {
            reject(`Schema callback error: ${error.message}`);
          }
        };
  
        request.onsuccess = (event) => {
          this.db = event.target.result;
          resolve(this.db);
        };
  
        request.onerror = (event) => reject(`Connection error: ${event.target.error}`);
      });
    }
  
    /**
     * Inserts data into an object store.
     * @param {string} table - The name of the object store.
     * @param {Object} data - The data to insert.
     * @returns {Promise<void>} Resolves when the operation is successful.
     */
    async insert(table, data) {
      return new Promise((resolve, reject) => {
        const transaction = this.db.transaction(table, "readwrite");
        const store = transaction.objectStore(table);
        const request = store.add(data);
  
        request.onsuccess = () => resolve();
        request.onerror = (event) => reject(`Insert error: ${event.target.error}`);
      });
    }
  
    /**
     * Retrieves all data from an object store that matches the filter callback.
     * @param {string} table - The name of the object store.
     * @param {function(Object): boolean} callback - Filter function.
     * @returns {Promise<Object[]>} Resolves to an array of matching data.
     */
    async select(table, callback) {
      return new Promise((resolve, reject) => {
        const transaction = this.db.transaction(table, "readonly");
        const store = transaction.objectStore(table);
        const request = store.getAll();
  
        request.onsuccess = (event) => {
          const results = event.target.result.filter(callback);
          resolve(results);
        };
  
        request.onerror = (event) => reject(`Select error: ${event.target.error}`);
      });
    }
  
    /**
     * Updates data in an object store by key.
     * @param {string} table - The name of the object store.
     * @param {IDBValidKey} key - The key of the data to update.
     * @param {Object} newData - The new data to replace the old data.
     * @returns {Promise<void>} Resolves when the operation is successful.
     */
    async update(table, key, newData) {
      return new Promise((resolve, reject) => {
        const transaction = this.db.transaction(table, "readwrite");
        const store = transaction.objectStore(table);
        const request = store.get(key);
  
        request.onsuccess = (event) => {
          const data = event.target.result;
          if (data) {
            Object.assign(data, newData);
            const updateRequest = store.put(data);
            updateRequest.onsuccess = () => resolve();
            updateRequest.onerror = (event) => reject(`Update error: ${event.target.error}`);
          } else {
            reject("Data not found.");
          }
        };
  
        request.onerror = (event) => reject(`Fetch error during update: ${event.target.error}`);
      });
    }
  
    /**
     * Deletes data from an object store by key.
     * @param {string} table - The name of the object store.
     * @param {IDBValidKey} key - The key of the data to delete.
     * @returns {Promise<void>} Resolves when the operation is successful.
     */
    async delete(table, key) {
      return new Promise((resolve, reject) => {
        const transaction = this.db.transaction(table, "readwrite");
        const store = transaction.objectStore(table);
        const request = store.delete(key);
  
        request.onsuccess = () => resolve();
        request.onerror = (event) => reject(`Delete error: ${event.target.error}`);
      });
    }
  
    /**
     * Performs a query on an object store with filtering, sorting, and limiting options.
     * @param {string} table - The name of the object store.
     * @param {Object} [options={}] - Query options (where, orderBy, limit).
     * @param {function(Object): boolean} [options.where] - Data filter function.
     * @param {string} [options.orderBy] - Field for sorting ("ASC" or "DESC").
     * @param {number} [options.limit] - Maximum number of data entries to retrieve.
     * @returns {Promise<Object[]>} Resolves to the query results.
     */
    async query(table, options = {}) {
      return new Promise((resolve, reject) => {
        const transaction = this.db.transaction(table, "readonly");
        const store = transaction.objectStore(table);
        const request = store.getAll();
  
        request.onsuccess = (event) => {
          let results = event.target.result;
  
          if (options.where) {
            results = results.filter(options.where);
          }
  
          if (options.orderBy) {
            const [field, direction] = options.orderBy.split(" ");
            results.sort((a, b) =>
              direction === "DESC" ? b[field] - a[field] : a[field] - b[field]
            );
          }
  
          if (options.limit) {
            results = results.slice(0, options.limit);
          }
  
          resolve(results);
        };
  
        request.onerror = (event) => reject(`Query error: ${event.target.error}`);
      });
    }
  
    /**
     * Performs a join operation between two tables based on a specified condition.
     * @param {string[]} tables - Array of table names to join (only 2 tables).
     * @param {function(Object, Object): boolean} joinCondition - Join condition between the two tables.
     * @param {function(Object, Object): Object} resultSelector - Function to map the join result.
     * @returns {Promise<Object[]>} Resolves to an array of join results.
     */
    async join(tables, joinCondition, resultSelector) {
      return new Promise(async (resolve, reject) => {
        try {
          const data = {};
  
          for (const table of tables) {
            data[table] = await this.select(table, () => true);
          }
  
          const results = [];
          for (const rowA of data[tables[0]]) {
            for (const rowB of data[tables[1]]) {
              if (joinCondition(rowA, rowB)) {
                results.push(resultSelector(rowA, rowB));
              }
            }
          }
  
          resolve(results);
        } catch (error) {
          reject(`Unexpected error during join: ${error.message}`);
        }
      });
    }
  
    /**
     * Performs an aggregation operation on a table.
     * @param {string} table - The table name.
     * @param {"SUM"|"AVG"|"COUNT"|"MAX"|"MIN"} operation - The aggregation operation.
     * @param {string} field - The field to calculate.
     * @returns {Promise<number>} Resolves to the aggregation result.
     */
    async aggregate(table, operation, field) {
      return new Promise((resolve, reject) => {
        const transaction = this.db.transaction(table, "readonly");
        const store = transaction.objectStore(table);
        const request = store.getAll();
  
        request.onsuccess = (event) => {
          const data = event.target.result;
          let result;
  
          switch (operation) {
            case "SUM":
              result = data.reduce((sum, item) => sum + (item[field] || 0), 0);
              break;
            case "AVG":
              result =
                data.reduce((sum, item) => sum + (item[field] || 0), 0) /
                data.length;
              break;
            case "COUNT":
              result = data.length;
              break;
            case "MAX":
              result = Math.max(...data.map((item) => item[field] || -Infinity));
              break;
            case "MIN":
              result = Math.min(...data.map((item) => item[field] || Infinity));
              break;
            default:
              reject("Invalid operation.");
              return;
          }
  
          resolve(result);
        };
  
        request.onerror = (event) => reject(`Aggregate error: ${event.target.error}`);
      });
    }
  }