@loopback/cli
Version: 
Yeoman generator for LoopBack 4
424 lines (364 loc) • 12 kB
JavaScript
// Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved.
// Node module: @loopback/cli
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
const _ = require('lodash');
const ArtifactGenerator = require('../../lib/artifact-generator');
const fs = require('fs');
const debug = require('../../lib/debug')('rest-crud-generator');
const inspect = require('util').inspect;
const path = require('path');
const chalk = require('chalk');
const utils = require('../../lib/utils');
const g = require('../../lib/globalize');
const cliPkg = require('../../lib/version-helper').cliPkg;
const {updateApplicationTs} = require('./crud-rest-component');
const VALID_CONNECTORS_FOR_REPOSITORY = ['PersistedModel'];
const REST_CONFIG_TEMPLATE = 'model.rest-config-template.ts.ejs';
const PROMPT_MESSAGE_MODEL = g.f(
  'Select the model(s) you want to generate a CRUD REST endpoint',
);
const PROMPT_MESSAGE_DATA_SOURCE = g.f('Please select the datasource');
const PROMPT_MESSAGE_BASE_PATH = g.f('Please specify the base path');
const PROMPT_MESSAGE_READONLY = g.f('Do you want to create readonly APIs?');
const ERROR_NO_DATA_SOURCES_FOUND = g.f('No datasources found at');
const ERROR_NO_MODELS_FOUND = g.f('No models found at');
const ERROR_NO_MODEL_SELECTED = g.f('You did not select a valid model');
module.exports = class RestCrudGenerator extends ArtifactGenerator {
  // Note: arguments and options should be defined in the constructor.
  constructor(args, opts) {
    super(args, opts);
  }
  /**
   * helper method to inspect and validate a datasource type
   */
  async _inferDataSourceNames() {
    if (!this.artifactInfo.dataSourceClass) {
      return;
    }
    this.artifactInfo.dataSourceName = utils.getDataSourceName(
      this.artifactInfo.datasourcesDir,
      this.artifactInfo.dataSourceClass,
    );
    this.artifactInfo.dataSourceClassName =
      utils.toClassName(this.artifactInfo.dataSourceName) + 'DataSource';
  }
  _setupGenerator() {
    this.artifactInfo = {
      type: 'rest-config',
      rootDir: utils.sourceRootDir,
    };
    this.artifactInfo.outDir = path.resolve(
      this.artifactInfo.rootDir,
      utils.modelEndpointsDir,
    );
    this.artifactInfo.datasourcesDir = path.resolve(
      this.artifactInfo.rootDir,
      utils.datasourcesDir,
    );
    this.artifactInfo.modelDir = path.resolve(
      this.artifactInfo.rootDir,
      utils.modelsDir,
    );
    this.artifactInfo.defaultTemplate = REST_CONFIG_TEMPLATE;
    this.option('model', {
      type: String,
      required: false,
      description: g.f('A valid model name'),
    });
    this.option('datasource', {
      type: String,
      required: false,
      description: g.f('A valid datasource name'),
    });
    this.option('basePath', {
      type: String,
      required: false,
      description: g.f('A valid base path'),
    });
    this.option('readonly', {
      type: Boolean,
      required: false,
      description: g.f('Create readonly APIs'),
      default: false,
    });
    return super._setupGenerator();
  }
  setOptions() {
    return super.setOptions();
  }
  checkLoopBackProject() {
    if (this.shouldExit()) return;
    return super.checkLoopBackProject();
  }
  async checkPaths() {
    if (this.shouldExit()) return;
    // check for datasources
    if (!fs.existsSync(this.artifactInfo.datasourcesDir)) {
      return this.exit(
        new Error(
          `${ERROR_NO_DATA_SOURCES_FOUND} ${this.artifactInfo.datasourcesDir}.
        ${chalk.yellow(
          'Please visit https://loopback.io/doc/en/lb4/DataSource-generator.html for information on how datasources are discovered',
        )}`,
        ),
      );
    }
    // check for models
    if (!fs.existsSync(this.artifactInfo.modelDir)) {
      return this.exit(
        new Error(
          `${ERROR_NO_MODELS_FOUND} ${this.artifactInfo.modelDir}.
        ${chalk.yellow(
          'Please visit https://loopback.io/doc/en/lb4/Model-generator.html for information on how models are discovered',
        )}`,
        ),
      );
    }
  }
  async promptDataSourceName() {
    if (this.shouldExit()) return false;
    debug('Prompting for a datasource ');
    let datasourcesList;
    // grab the datasourcename from the command line
    const cmdDatasourceName = this.options.datasource
      ? utils.toClassName(this.options.datasource) + 'Datasource'
      : '';
    debug(`command line datasource is  ${cmdDatasourceName}`);
    try {
      datasourcesList = await utils.getArtifactList(
        this.artifactInfo.datasourcesDir,
        'datasource',
        true,
      );
      debug(
        `datasourcesList from ${utils.sourceRootDir}/${utils.datasourcesDir} : ${datasourcesList}`,
      );
    } catch (err) {
      return this.exit(err);
    }
    const availableDatasources = datasourcesList.filter(item => {
      debug(`data source inspecting item: ${item}`);
      const result = utils.isConnectorOfType(
        VALID_CONNECTORS_FOR_REPOSITORY,
        this.artifactInfo.datasourcesDir,
        item,
      );
      return result !== false;
    });
    debug(`artifactInfo.dataSourceClass ${this.artifactInfo.dataSourceClass}`);
    if (availableDatasources.length === 0) {
      return this.exit(
        new Error(
          `${ERROR_NO_DATA_SOURCES_FOUND} ${this.artifactInfo.datasourcesDir}.
        ${chalk.yellow(
          'Please visit https://loopback.io/doc/en/lb4/DataSource-generator.html for information on how datasources are discovered',
        )}`,
        ),
      );
    }
    if (availableDatasources.includes(cmdDatasourceName)) {
      Object.assign(this.artifactInfo, {
        dataSourceClass: cmdDatasourceName,
      });
    }
    return this.prompt([
      {
        type: 'list',
        name: 'dataSourceClass',
        message: PROMPT_MESSAGE_DATA_SOURCE,
        choices: availableDatasources,
        when: !this.artifactInfo.dataSourceClass,
        default: availableDatasources[0],
        validate: utils.validateClassName,
      },
    ])
      .then(props => {
        Object.assign(this.artifactInfo, props);
        debug(`props after datasource prompt: ${inspect(props)}`);
        return props;
      })
      .catch(err => {
        debug(`Error during datasource prompt: ${err}`);
        return this.exit(err);
      });
  }
  async promptModels() {
    if (this.shouldExit()) return false;
    await this._inferDataSourceNames();
    let modelList;
    try {
      debug(`model list dir ${this.artifactInfo.modelDir}`);
      modelList = await utils.getArtifactList(
        this.artifactInfo.modelDir,
        'model',
      );
    } catch (err) {
      return this.exit(err);
    }
    if (this.options.model) {
      debug(`Model name received from command line: ${this.options.model}`);
      this.options.model = utils.toClassName(this.options.model);
      // assign the model name from the command line only if it is valid
      if (
        modelList &&
        modelList.length > 0 &&
        modelList.includes(this.options.model)
      ) {
        Object.assign(this.artifactInfo, {modelNameList: [this.options.model]});
      } else {
        modelList = [];
      }
    }
    if (modelList.length === 0) {
      return this.exit(
        new Error(
          `${ERROR_NO_MODELS_FOUND} ${this.artifactInfo.modelDir}.
        ${chalk.yellow(
          'Please visit https://loopback.io/doc/en/lb4/Model-generator.html for information on how models are discovered',
        )}`,
        ),
      );
    }
    return this.prompt([
      {
        type: 'checkbox',
        name: 'modelNameList',
        message: PROMPT_MESSAGE_MODEL,
        choices: modelList.map(m => ({name: m, value: m, checked: true})),
        when: this.artifactInfo.modelNameList === undefined,
        // Require at least one model to be selected
        // This prevents users from accidentally pressing ENTER instead of SPACE
        // to select a model from the list
        validate: result => !!result.length,
      },
    ])
      .then(props => {
        Object.assign(this.artifactInfo, props);
        debug(`props after model list prompt: ${inspect(props)}`);
        return props;
      })
      .catch(err => {
        debug(`Error during model list prompt: ${err}`);
        return this.exit(err);
      });
  }
  /**
   * Prompt for basePath if only one model is selected
   */
  async promptBasePath() {
    if (this.options.basePath) {
      this.artifactInfo.basePath = this.options.basePath;
      return;
    }
    if (
      this.artifactInfo.modelNameList &&
      this.artifactInfo.modelNameList.length === 1
    ) {
      const model = this.artifactInfo.modelNameList[0];
      const props = await this.prompt([
        {
          type: 'input',
          name: 'basePath',
          message: PROMPT_MESSAGE_BASE_PATH,
          default: utils.prependBackslash(
            utils.pluralize(utils.urlSlug(model)),
          ),
          validate: utils.validateUrlSlug,
          filter: utils.prependBackslash,
        },
      ]);
      if (props) {
        this.artifactInfo.basePath = props.basePath;
      }
    }
  }
  async promptReadonly() {
    const props = await this.prompt([
      {
        type: 'confirm',
        name: 'readonly',
        message: PROMPT_MESSAGE_READONLY,
        default: false,
      },
    ]);
    if (props) {
      this.artifactInfo.readonly = props.readonly;
    }
  }
  async scaffold() {
    if (this.shouldExit()) return false;
    if (_.isEmpty(this.artifactInfo.modelNameList)) {
      return this.exit(new Error(`${ERROR_NO_MODEL_SELECTED}`));
    }
    this.artifactInfo.disableIndexUpdate = true;
    for (const model of this.artifactInfo.modelNameList) {
      this.artifactInfo.modelName = model;
      this.artifactInfo.outFile = utils.getRestConfigFileName(model);
      if (
        !this.artifactInfo.basePath ||
        this.artifactInfo.modelNameList.length > 1
      ) {
        const defaultBasePath =
          this.options.basePath ||
          utils.prependBackslash(utils.pluralize(utils.urlSlug(model)));
        this.artifactInfo.basePath = defaultBasePath;
      }
      const source = this.templatePath(
        path.join(
          utils.sourceRootDir,
          utils.modelEndpointsDir,
          this.artifactInfo.defaultTemplate,
        ),
      );
      const dest = this.destinationPath(
        path.join(this.artifactInfo.outDir, this.artifactInfo.outFile),
      );
      if (debug.enabled) {
        debug(`artifactInfo: ${inspect(this.artifactInfo)}`);
        debug(`Copying artifact to: ${dest}`);
      }
      this.copyTemplatedFiles(source, dest, this.artifactInfo);
    }
    this.log(
      'Updating src/application.ts to register CrudRestComponent from @loopback/rest-crud',
    );
    await updateApplicationTs(this.destinationPath());
    return;
  }
  install() {
    if (this.shouldExit()) return false;
    debug('install npm dependencies');
    const pkgJson = this.packageJson || {};
    const deps = pkgJson.get('dependencies') || {};
    const pkgs = [];
    const version = cliPkg.config.templateDependencies['@loopback/rest-crud'];
    if (!deps['@loopback/rest-crud']) {
      pkgs.push(`/rest-crud@${version}`);
    }
    if (pkgs.length === 0) return;
    this.pkgManagerInstall(pkgs, {
      npm: {
        save: true,
      },
    });
  }
  async end() {
    this.artifactInfo.type =
      this.artifactInfo.modelNameList &&
      this.artifactInfo.modelNameList.length > 1
        ? 'RestConfigs'
        : 'RestConfig';
    this.artifactInfo.modelNameList = _.map(
      this.artifactInfo.modelNameList,
      name => {
        return name + '.rest-config';
      },
    );
    this.artifactInfo.name = this.artifactInfo.modelNameList
      ? this.artifactInfo.modelNameList.join(this.classNameSeparator)
      : this.artifactInfo.modelName;
    await super.end();
  }
};