Home Manual Reference Source Test API Healthcheck

src/helpers/mariadb.helper.js

/**
 * @flow
 */
import mariadb from 'mariadb';
import CONFIG from './config.helper.js';


// Disable weak type rule to allow for generalized database objects
/* eslint-disable flowtype/no-weak-types, flowtype/require-variable-type */

/**
 * Configuration for MariaDBHelper Database connection
 * @type {MariaDBConfig}
 */
export interface MariaDBConfig {
  host: string;
  user: string;
  password: string;
  connectionLimit: number;
};

/**
 * Response from MariaDB Insert statement (From MariaDB)
 * @type {MariaDBInsertResponse}
 */
export interface MariaDBInsertResponse {
  affectedRows: number;
  insertId: number;
  warningStatus: number;
}

/**
 * MariaDB Helper for easy access to a configured MySQL Connection, with consistent methods for simple
 * interactions and larger transactions.
 *
 * This class can also be instantiated pointing elsewhere with a different MariaDBConfig passed to the
 * constructor
 * @type {MariaDBHelper}
 */
export class MariaDBHelper {
  /**
   * MariaDB Pool API
   * @type {PoolApi} https://mariadb.com/kb/en/library/connector-nodejs-promise-api/#pool-api
   */
  dbPool: any;
  transactionConnection: any;

  /**
   * Creates a new Helper (on this file export starts the Database Pool)
   * @param {MariaDBConfig} config                Configuration to Connect to MySQL Database
   * @param {MariaDB.Connection} transactionConnection MariaDB Connection to use for larger transactions
   */
  constructor({ config, transactionConnection }: { config?: MariaDBConfig, transactionConnection?: any }) {
    // NOTE: Not sure if this is better, or if we should just make sure to always call `shutdown` to close db
    // If testing, skip any startup
    if (process.argv.includes('--env.unit_test')) {
      return;
    }

    if (config) {
      // Instantiating a connection to a different server
      this.dbPool = mariadb.createPool(config);
    } else if (transactionConnection) {
      // For Larger Transaction
      // TODO: Finish up transactionConnection Helper
    } else {
      // TODO: Log on failure to create MariaDB Helper
      throw new Error('MariaDBHelper created without DB Config or connection to use');
    }
  }

  /**
   * Shuts down the mariadb connections
   * @param  {Function} callback callback function to call after shutting down db connections
   * @return {undefined}            no return
   */
  shutdown(callback: Function) {
    if (this.dbPool) {
      this.dbPool.end((err: Error) => {
        if (err) {
          // TODO: Do Something on MariaDB Shutdown error
        }
        callback(err);
      });
    }
  }

  /**
   * Return the status of the database server (should be called on an instance with a pool, not connection)
   * @return {String | boolean}     Either the current time, returned from database, or false if unable
   * to connect
   */
  async getStatus(): Promise<string | boolean> {
    if (this.dbPool) {
      return await this.dbPool.query('SELECT NOW();');
    } else {
      return false;
    }
  }

  /**
   * Execute a simple query on the entire database
   * @param  {string}  query Query string to be passed to DB for response
   *  see https://mariadb.com/kb/en/library/data-manipulation/ for reference
   * @param  {Array<any>}      values Array of values to inject into the query string, matching the number of
   *  placeholders given
   * @return {Promise}        Returns the response from the query
   */
  query(query: string, values?: Array<any>): Promise<any> {
    if (this.transactionConnection) {
      // TODO: use transaction connection if it exists (this is what makes this so versatile)
      return this.transactionConnection.query(query, values);
    } else {
      return this.dbPool.query(query, values);
    }
  }

  /**
   * Fetch all rows given a query string and values that can qualify the query
   * @param  {string}  query  Query string to pass to mariadb
   * @return {Promise}        Return array of values
   */
  async fetch(query: string): Promise<Array<any>> {
    // TODO: Log if query doesn't contain `SELECT`

    const response = await this.query(query);

    if (!response || response.length === 0) {
      return [];
    }
    return response;
  }

  /**
   * Fetch the (first) row that matches a string
   * @param  {string}  query  Query string to pass to mariadb
   * @return {Promise}        Returns a single value
   */
  async fetchOne(query: string): Promise<any> {
    const response = await this.fetch(query);

    if (!response || response.length === 0) {
      return undefined;
    }

    return response[0];
  }

  /**
   * Insert an Object into a specific table
   * @param  {String}  table  Name of table to insert object into
   * @param  {Object}  object Mapping from column names to values to insert into DB Table
   * @return {Promise<MariaDBInsertResponse>}        Response from MariaDB
   */
  async insert(table: string, object: Object): Promise<MariaDBInsertResponse> {
    // e.g. {'abc': 1, 'def', 2 }
    const keyString: string = Object.keys(object).join(', '); // 'abc, def'

    const values: Array<any> = Object.values(object); // [1, 2]
    const inserts: string = '?,'.repeat(values.length).slice(0, (values.length * 2) - 1); // '?, ?'

    const response: MariaDBInsertResponse = await this.query(
      `INSERT INTO ${table} (${keyString}) value (${inserts})`,
      values
    );

    return response;
  }

  /**
   * Insert multiple objects into the table
   * @param  {string}  table   to insert into
   * @param  {Array<Object>}  objects Array of objects to insert
   * @return {Promise}        Response from MariaDB
   */
  async insertMultiple(table: string, objects: Array<Object>): Promise<any> {
    // e.g. {'abc': 1, 'def', 2 }
    const firstObject = objects[0];
    const keyString: string = Object.keys(firstObject).join(', '); // 'abc, def'

    let inserts = '';
    const allValues = [];
    for (const object of objects) {
      const values: Array<any> = Object.values(object); // [1, 2]
      inserts = '?,'.repeat(values.length).slice(0, (values.length * 2) - 1); // '?, ?'
      allValues.push(values);
    }

    // TODO: Handle insert if in transaction
    if (this.transactionConnection) {
      // just use transactionConnection instead?
    } else {
      // Best way to insert multiple objects in one statement?
      return await this.dbPool.batch(`INSERT INTO ${table} (${keyString}) VALUES (${inserts})`, allValues);
    }
  }

  // async update(table: string, object: any, where: any): any {
  //  // TODO: Write proper update statement
  // }

  // /**
  //  * Create a new connection and Helper to run a single transaction with multiple queries/db statements
  //  * @param  {Function} transactionCallback Uses the helper passed back to run all of the different queries
  //  * @return {Promise}                     Indicates that the transaction has completed or errored
  //  */
  // transaction(transactionCallback: Function): Promise<any> {
  //   console.log(transactionCallback);
  //   if (this.dbPool) { // TODO: Create new Helper with a connection from pool }
  //
  //   return new Promise((resolve: Function, reject: Function) => {
  //     console.log(this);
  //     this._beginTransaction();
  //
  //     // TODO: call transactionCallback(with new helper)
  //
  //     // TODO: close connection properly
  //
  //     if (failure)
  //       this._rollbackTransaction();
  //       console.log(reject);
  //
  //     else
  //       this._commitTransaction();
  //       console.log(resolve);
  //
  //   });
  // }

  // async _beginTransaction() {
  //
  // }
  //
  // async _commitTransaction() {
  //
  // }
  //
  // async _rollbackTransaction() {
  //
  // }

  // async healthCheck() {
  //
  // }
}


const mariadbHelper = new MariaDBHelper({ config: {
  database: undefined,
  host: CONFIG.DB.MARIADB.host,
  user: CONFIG.DB.MARIADB.user,
  password: CONFIG.DB.MARIADB.password,
  connectionLimit: 5
} });

/**
 * Exported MariaDB Helper that uses DB configuration in config.yaml
 * @type {MariaDBHelper}
 */
export default mariadbHelper;