@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();
}
};