Home Manual Reference Source

src/abstracts/configurationFile.js

const fs = require('fs-extra');
const ObjectUtils = require('wootils/shared/objectUtils');
const path = require('path');
/**
 * A helper class for creating configuration files that can be overwritten on
 * implementation.
 * @abstract
 * @version 1.0
 */
class ConfigurationFile {
  /**
   * Class constructor.
   * @param {PathUtils}         pathUtils            To build the path to the overwrite file.
   * @param {string|Array}      overwritePaths       A path of a list of paths for files that can
   *                                                 overwrite the configuration. If used as a
   *                                                 string, it will assume the path is inside
   *                                                 the `config` folder, but if used as a list, the
   *                                                 paths will be relative to the project root
   *                                                 directory.
   *                                                 If used as an array, the class will use the
   *                                                 first file of the list that exists and ignore
   *                                                 the rest.
   * @param {boolean}           [asFactory=false]    If `true`, every time `getConfig` gets called,
   *                                                 the configuration will be created again,
   *                                                 instead of caching it the first time it's
   *                                                 created.
   * @param {?ConfigurationFile} [parentConfig=null] If this parameter is used, the configuration
   *                                                 created by the instance will be merged on top
   *                                                 of the configuration returned by the
   *                                                 `getConfig` method of the parent configuration.
   * @throws {TypeError} If instantiated directly.
   * @abstract
   */
  constructor(pathUtils, overwritePaths, asFactory = false, parentConfig = null) {
    if (new.target === ConfigurationFile) {
      throw new TypeError(
        'ConfigurationFile is an abstract class, it can\'t be instantiated directly'
      );
    }
    /**
     * A local reference for the `pathUtils` service.
     * @type {PathUtils}
     */
    this.pathUtils = pathUtils;
    /**
     * A list of paths that can overwrite the configuration.
     * @type {Array}
     */
    this.overwritePaths = (typeof overwritePaths === 'string') ?
      [path.join('config', overwritePaths)] :
      (overwritePaths || []);
    /**
     * Whether the configuration should be created every time `getConfig` gets called or not.
     * @type {boolean}
     */
    this.asFactory = asFactory;
    /**
     * A parent configuration to extend.
     * @type {?ConfigurationFile}
     */
    this.parentConfig = parentConfig;
    /**
     * This will store the configuration after creating it.
     * @type {?Object}
     * @ignore
     * @access protected
     */
    this._config = null;
    /**
     * A flag to know if the overwrite file has been loaded or not.
     * @type {boolean}
     * @ignore
     * @access protected
     */
    this._fileConfigLoaded = false;
    /**
     * A function that eventually will return the changes from the overwrite file. Once the file
     * is loaded, if the file exports a function, then it will replace this variable, otherwise, the
     * return value of this method will be become the exported configuration.
     * @return {Object}
     * @ignore
     * @access protected
     */
    this._fileConfig = () => ({});
  }
  /**
   * This method will be called the first time `getConfig` gets called (or every time, depending on
   * the value of the `asFactory` property) and it should return the configuration contents.
   * As parameters, it will return the same ones sent to `getConfig`.
   * @example
   * // Let's say the class receives this call: `getConfig({ name: 'Charito'}, 'hello')`, you could
   * // do something like this:
   * createConfig(options, prefix) {
   *   return { message: `${prefix} ${options.name}` };
   * }
   * // And the configuration would be `{ message: 'hello Charito'}`
   * @throws {Error} if not overwritten.
   * @abstract
   */
  createConfig() {
    throw new Error('This method must to be overwritten');
  }
  /**
   * This is the public method all other services uses to obtain the configuration. If the
   * configuration doesn't exists or `asFactory` was set to `true` on the `constructor`, the
   * configuration will be reloaded.
   * @param  {Array} args A list of parameters for the service to use when creating the
   *                      configuration
   * @return {Object}
   */
  getConfig(...args) {
    if (!this._config || this.asFactory) {
      this._loadConfig(...args);
    }

    return this._config;
  }
  /**
   * This is the real method that creates the configuration.
   * @param  {Array} args A list of parameters for the service to use when creating the
   *                      configuration
   * @ignore
   * @access protected
   */
  _loadConfig(...args) {
    // If the overwrite file wasn't loaded yet...
    if (!this._fileConfigLoaded) {
      // ...turn on the flag that says it was loaded.
      this._fileConfigLoaded = true;
      // Call the method that loads the file.
      this._loadConfigFromFile();
    }

    let parentConfig = {};
    // If a parent configuration was defined on the constructor...
    if (this.parentConfig) {
      /**
       * Get its configuration by calling its `getConfig` method with the same parameters this
       * method received.
       */
      parentConfig = this.parentConfig.getConfig(...args);
    }
    // Define the current configuration using the parent one.
    let currentConfig = ObjectUtils.copy(parentConfig);
    // Create a new set of arguments by adding the current configuration at the end.
    let currentArgs = [...args, currentConfig];
    // Update the current configuration by calling `createConfig` with the new arguments.
    currentConfig = ObjectUtils.merge(currentConfig, this.createConfig(...currentArgs));
    // Update the arguments with the "new current configuration".
    currentArgs = [...args, currentConfig];
    // Finally, call the method for the overwrite file and merge everything together.
    this._config = ObjectUtils.merge(currentConfig, this._fileConfig(...currentArgs));
  }
  /**
   * Load the configuration from an overwrite file.
   * @ignore
   * @access protected
   */
  _loadConfigFromFile() {
    const filepath = this.overwritePaths
    .map((overwrite) => this.pathUtils.join(overwrite))
    .find((overwrite) => fs.pathExistsSync(overwrite));
    // If there's a file...
    if (filepath) {
      // ...require it
      // eslint-disable-next-line global-require, import/no-dynamic-require
      const overwriteContents = require(filepath);
      // If the file exported anything...
      if (overwriteContents) {
        // ...get the type of whatever the file exported.
        const overwriteType = typeof overwriteContents;
        // If the file exported a function...
        if (overwriteType === 'function') {
          // ...set it as the `_fileConfig` property.
          this._fileConfig = overwriteContents;
        } else {
          // ...otherwise, set the `_fileConfig` property to return whatever the file exported.
          this._fileConfig = () => overwriteContents;
        }
      }
    }
  }
}

module.exports = ConfigurationFile;