Home Reference Source Test

src/lib/core/installer.js

import fs from 'fs';
import path from 'path';
import memFs from 'mem-fs';
import editor from 'mem-fs-editor';
import { Subject } from 'rxjs';

import { defaultUserGeneratorConfig, systemSeederTemplate } from '../constants';
import config from '../config';

import InstallerError from './installer-error';

/**
 * mongoose-data-seed installer
 *
 * @example
 * // create installer
 * const installer = new Installer({ seedersFolder: './seeders' });
 *
 * // run seeders
 * const observable = installer.install();
 *
 * // subscribe logger
 * observable.subscribe({
 *   next({ type, payload }) {
 *     switch (type) {
 *       case Installer.operations.START:
 *         console.log('Installer started!');
 *         break;
 *       case Installer.operations.SUCCESS:
 *         console.log('Installer finished successfully!');
 *         break;
 *     }
 *   },
 *   error({ type, payload }) {
 *     console.error(`Error: ${type}`);
 *     console.error(payload.error);
 *   }
 * });
 */
export default class Installer {
  /**
   * @typedef {Object} InstallerConfig
   * @property {string}  seedersFolder        Relative path to your seeders-folder.
   * @property {?string} customSeederTemplate Relative path to your seeder-template
   *                                          if you would like to use your own seeders-template
   */

  /**
   * Installer operations constants
   * @type {Object}
   * @property {string} START        Installation starts.
   * @property {string} SUCCESS      Installation succeed.
   * @property {string} ERROR        Installation finished with an error.
   */
  static operations = {
    START: 'START',
    SUCCESS: 'SUCCESS',
    ERROR: 'ERROR',

    WRITE_USER_GENERETOR_CONFIG_START: 'WRITE_USER_GENERETOR_CONFIG_START',
    WRITE_USER_GENERETOR_CONFIG_SUCCESS: 'WRITE_USER_GENERETOR_CONFIG_SUCCESS',
    WRITE_USER_GENERETOR_CONFIG_ERROR: 'WRITE_USER_GENERETOR_CONFIG_ERROR',

    CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START:
      'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START',
    CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS:
      'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS',
    CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_ERROR:
      'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_ERROR',
    CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS:
      'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS',
    CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM:
      'CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM',

    CREATE_SEEDERS_FOLDER_START: 'CREATE_SEEDERS_FOLDER_START',
    CREATE_SEEDERS_FOLDER_SUCCESS: 'CREATE_SEEDERS_FOLDER_SUCCESS',
    CREATE_SEEDERS_FOLDER_ERROR: 'CREATE_SEEDERS_FOLDER_ERROR',
    CREATE_SEEDERS_FOLDER_SKIP_FOLDER_EXISTS:
      'CREATE_SEEDERS_FOLDER_SKIP_FOLDER_EXISTS',

    WRITE_USER_CONFIG_START: 'WRITE_USER_CONFIG_START',
    WRITE_USER_CONFIG_SUCCESS: 'WRITE_USER_CONFIG_SUCCESS',
    WRITE_USER_CONFIG_ERROR: 'WRITE_USER_CONFIG_ERROR',
    WRITE_USER_CONFIG_SKIP_FILE_EXISTS: 'WRITE_USER_CONFIG_SKIP_FILE_EXISTS',
  };

  /**
   * Creates mongoose-data-seed installer
   * @param {InstallerConfig} [config] Generator config
   */
  constructor({
    seedersFolder,
    customSeederTemplate,
  } = defaultUserGeneratorConfig) {
    this._subject = new Subject();
    this._initConfig({ seedersFolder, customSeederTemplate });
    this._initMemFs();
  }

  /**
   * Run installer - install `mongoose-data-seeder`
   * @return {Observable}
   * @see https://rxjs-dev.firebaseapp.com/api/index/class/Observable
   */
  install() {
    this._install();

    return this._subject.asObservable();
  }

  /**
   * get the config that should be written into the `package.json`
   * @return {InstallerConfig} generator config
   */
  getGeneratorConfig() {
    const {
      userSeedersFolderName: seedersFolder,
      customSeederTemplateFilename: customSeederTemplate,
    } = this.config;

    const generatorConfig = { seedersFolder };

    if (customSeederTemplate) {
      generatorConfig.customSeederTemplate = customSeederTemplate;
    }

    return generatorConfig;
  }

  /*
    Private methods
   */

  /**
   * Initiate this.config
   * @param {InstallerConfig} config generator config
   */
  _initConfig({ seedersFolder, customSeederTemplate }) {
    /**
     * Full configuration object
     * @type {Object}
     * @property {string}  userPackageJsonPath path to the user package.json file.
     * @property {?string} customSeederTemplateFilename custom seeder template filename.
     * @property {?string} customSeederTemplatePath custom seeder template path.
     * @property {string}  userSeedersFolderName seeders folder name.
     * @property {string}  userSeedersFolderPath seeders folder path.
     * @property {boolean} userConfigExists user has a config file?.
     * @property {?string} userConfigFilename config file name.
     * @property {?string} userConfigFilepath config file path.
     * @property {string}  configTemplatePath config template path.
     */
    this.config = {
      userPackageJsonPath: path.join(config.projectRoot, './package.json'),
      customSeederTemplateFilename:
        customSeederTemplate && customSeederTemplate,
      customSeederTemplatePath:
        customSeederTemplate &&
        path.join(config.projectRoot, customSeederTemplate),
      userSeedersFolderName: seedersFolder,
      userSeedersFolderPath: path.join(config.projectRoot, seedersFolder),
      userConfigExists: config.userConfigExists,
      userConfigFilename: config.userConfigFilename,
      userConfigFilepath: config.userConfigFilepath,
      configTemplatePath: config.configTemplate,
    };
  }

  /**
   * Initiate the in-memory file-system
   */
  _initMemFs() {
    const store = memFs.create();
    this._memFsEditor = editor.create(store);
  }

  /**
   * Run the installation process
   * @return {Promise}
   */
  async _install() {
    const { START, SUCCESS, ERROR } = Installer.operations;

    try {
      this._subject.next({ type: START });

      await this._createCustomSeederTemplate();
      await this._writeUserGeneratorConfigToPackageJson();
      await this._createSeedersFolder();
      await this._writeUserConfig();

      this._subject.next({ type: SUCCESS });

      this._subject.complete();
    } catch (error) {
      const { type = ERROR, payload = { error } } = error;

      this._subject.error({ type, payload });
    }
  }

  /**
   * Commit the in-memory file changes
   * @return {Promise}
   */
  async _commitMemFsChanges() {
    return new Promise(resolve => {
      this._memFsEditor.commit(() => {
        resolve();
      });
    });
  }

  /**
   * Copy the package seeder-template to the user desired
   * custom-seeder-template path if the user wants to use his own seeder-template
   * @return {Promise} [description]
   */
  async _createCustomSeederTemplate() {
    const {
      CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START,
      CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS,
      CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_ERROR,
      CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS,
      CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM,
    } = Installer.operations;

    const {
      customSeederTemplateFilename,
      customSeederTemplatePath,
    } = this.config;

    const payload = { customSeederTemplateFilename, customSeederTemplatePath };

    const notify = type => this._subject.next({ type, payload });

    try {
      notify(CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_START);

      if (!customSeederTemplatePath) {
        return notify(CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_NO_CUSTOM);
      }

      if (fs.existsSync(customSeederTemplatePath)) {
        notify(CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SKIP_FILE_EXISTS);
      } else {
        // copy template
        this._memFsEditor.copy(systemSeederTemplate, customSeederTemplatePath);
        // commit changes
        await this._commitMemFsChanges();

        notify(CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_SUCCESS);
      }
    } catch (error) {
      throw new InstallerError({
        type: CREATE_CUSTOM_SEEDER_TEMPLATE_FILE_ERROR,
        payload,
        error,
      });
    }
  }

  /**
   * Write the config into the user package.json
   */
  async _writeUserGeneratorConfigToPackageJson() {
    const {
      WRITE_USER_GENERETOR_CONFIG_START,
      WRITE_USER_GENERETOR_CONFIG_SUCCESS,
      WRITE_USER_GENERETOR_CONFIG_ERROR,
    } = Installer.operations;

    const { userPackageJsonPath: packageJsonPath } = this.config;

    const payload = { packageJsonPath };

    try {
      this._subject.next({ type: WRITE_USER_GENERETOR_CONFIG_START, payload });

      const packageJson = require(packageJsonPath);
      packageJson.mdSeed = this.getGeneratorConfig();

      this._memFsEditor.writeJSON(packageJsonPath, packageJson);

      await this._commitMemFsChanges();

      this._subject.next({
        type: WRITE_USER_GENERETOR_CONFIG_SUCCESS,
        payload,
      });
    } catch (error) {
      throw new InstallerError({
        type: WRITE_USER_GENERETOR_CONFIG_ERROR,
        payload,
        error,
      });
    }
  }

  /**
   * Create the seeders folder
   */
  async _createSeedersFolder() {
    const {
      CREATE_SEEDERS_FOLDER_START,
      CREATE_SEEDERS_FOLDER_SUCCESS,
      CREATE_SEEDERS_FOLDER_ERROR,
      CREATE_SEEDERS_FOLDER_SKIP_FOLDER_EXISTS,
    } = Installer.operations;

    const {
      userSeedersFolderPath: folderpath,
      userSeedersFolderName: foldername,
    } = this.config;

    const payload = { folderpath, foldername };

    try {
      this._subject.next({ type: CREATE_SEEDERS_FOLDER_START, payload });

      if (fs.existsSync(folderpath)) {
        this._subject.next({
          type: CREATE_SEEDERS_FOLDER_SKIP_FOLDER_EXISTS,
          payload,
        });
      } else {
        fs.mkdirSync(folderpath);

        this._subject.next({ type: CREATE_SEEDERS_FOLDER_SUCCESS, payload });
      }
    } catch (error) {
      throw new InstallerError({
        type: CREATE_SEEDERS_FOLDER_ERROR,
        payload,
        error,
      });
    }
  }

  /**
   * Write the `md-seed-config.js` into the root folder
   */
  async _writeUserConfig() {
    const {
      WRITE_USER_CONFIG_START,
      WRITE_USER_CONFIG_SUCCESS,
      WRITE_USER_CONFIG_ERROR,
      WRITE_USER_CONFIG_SKIP_FILE_EXISTS,
    } = Installer.operations;

    const {
      userConfigExists: fileExists,
      userConfigFilename: filename,
      userConfigFilepath: filepath,
      configTemplatePath,
    } = this.config;

    const payload = { fileExists, filename, filepath };

    try {
      this._subject.next({ type: WRITE_USER_CONFIG_START, payload });

      if (fileExists === true) {
        this._subject.next({
          type: WRITE_USER_CONFIG_SKIP_FILE_EXISTS,
          payload,
        });
      } else {
        // copy template
        this._memFsEditor.copy(configTemplatePath, filepath);
        // commit changes
        await this._commitMemFsChanges();

        this._subject.next({ type: WRITE_USER_CONFIG_SUCCESS, payload });
      }
    } catch (error) {
      throw new InstallerError({
        type: WRITE_USER_CONFIG_ERROR,
        payload,
        error,
      });
    }
  }
}