@loopback/cli
Version:
Yeoman generator for LoopBack 4
349 lines (307 loc) • 10.4 kB
JavaScript
// Copyright IBM Corp. and LoopBack contributors 2018,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
// no translation: Datasource
'use strict';
const ArtifactGenerator = require('../../lib/artifact-generator');
const debug = require('../../lib/debug')('datasource-generator');
const chalk = require('chalk');
const path = require('path');
const utils = require('../../lib/utils');
const connectors = require('../../lib/connectors.json');
const g = require('../../lib/globalize');
/**
* DataSource Generator -- CLI
*
* Prompts for a name, connector and connector options. Creates json file
* for the DataSource as well as a Class for a user to modify. Also installs the
* appropriate connector from npm.
*/
module.exports = class DataSourceGenerator extends ArtifactGenerator {
constructor(args, opts) {
super(args, opts);
}
_setupGenerator() {
this.artifactInfo = {
type: 'datasource',
rootDir: 'src',
};
// Datasources are stored in the datasources directory
this.artifactInfo.outDir = path.resolve(
this.artifactInfo.rootDir,
'datasources',
);
const connectorChoices = [];
/**
* Creating a list of connectors -- and marking them as either supported by
* StrongLoop or community.
*/
Object.values(connectors).forEach(connector => {
const support = connector.supportedByStrongLoop
? '(supported by StrongLoop)'
: '(provided by community)';
connectorChoices.push({
name: `${connector.description} ${chalk.gray(support)}`,
value: connector.name,
});
});
this.connectorChoices = connectorChoices;
// Add `other` so users can add a connector that isn't part of the list
// Though it can be added by creating a PR and adding it to
// connectors.json
this.connectorChoices.push('other');
return super._setupGenerator();
}
setOptions() {
// remove not needed .env property
if (this.options.config) {
delete this.options?.env;
}
this.artifactInfo.connector = this.options.connector;
return super.setOptions();
}
/**
* Ensure CLI is being run in a LoopBack 4 project.
*/
checkLoopBackProject() {
if (this.shouldExit()) return;
return super.checkLoopBackProject();
}
/**
* Ask for DataSource Name -- Must be unique
*/
promptArtifactName() {
debug('Prompting for artifact name');
if (this.shouldExit()) return false;
if (this.artifactInfo.name) {
return;
}
const prompts = [
{
type: 'input',
name: 'name',
// capitalization
message: g.f('%s name:', utils.toClassName(this.artifactInfo.type)),
when: this.artifactInfo.name === undefined,
validate: utils.validateClassName,
},
];
return this.prompt(prompts).then(props => {
Object.assign(this.artifactInfo, props);
return props;
});
}
/**
* Ask the user to select the connector for the DataSource
*/
promptConnector() {
debug('prompting for datasource connector');
if (this.shouldExit()) return;
const prompts = [
{
name: 'connector',
message: g.f(
'Select the connector for %s: ',
chalk.yellow(this.artifactInfo.name),
),
type: 'list',
default: 'memory',
choices: this.connectorChoices,
when: this.artifactInfo.connector === undefined,
},
];
return this.prompt(prompts).then(props => {
Object.assign(this.artifactInfo, props);
return props;
});
}
/**
* If the user selected `other` for connector -- ask the user to provide
* `npm` module name for the connector.
*/
promptCustomConnectorInfo() {
if (this.shouldExit()) return;
if (this.artifactInfo.connector !== 'other') {
debug('custom connector option was not selected');
return;
} else {
debug('prompting for custom connector');
const prompts = [
{
name: 'customConnector',
message: g.f("Enter the connector's package name:"),
validate: utils.validate,
},
];
return this.prompt(prompts).then(props => {
this.artifactInfo.connector = props.customConnector;
return props;
});
}
}
/**
* Prompt the user for connector specific settings -- only applies to
* connectors in the connectors.json list
*/
promptConnectorConfig() {
debug('prompting for connector config');
if (this.shouldExit()) return;
// Check to make sure connector is from connectors list (not custom)
const settings =
(connectors[this.artifactInfo.connector] &&
connectors[this.artifactInfo.connector]['settings']) ||
{};
const prompts = [];
// Create list of questions to prompt the user
Object.entries(settings).forEach(([key, setting]) => {
// Set defaults and merge with `setting` to override properties
const question = Object.assign(
{},
{name: key, message: key, suffix: ':', default: null},
setting,
);
/**
* Allowed Types: string, number, password, object, array, boolean
* Must be converted to inquirer types -- input, confirm, password
*/
switch ((setting.type || '').toLowerCase()) {
case 'string':
case 'number':
question.type = 'input';
break;
case 'object':
case 'array':
question.type = 'input';
question.validate = utils.validateStringObject(setting.type);
break;
case 'boolean':
question.type = 'confirm';
break;
case 'password':
break;
default:
console.warn(
g.f(
'Using default input of type input for setting %s as %s is not supported',
key,
setting.type || undefined,
),
);
// Default to input type
question.type = 'input';
}
prompts.push(question);
});
debug(`connector setting questions - ${JSON.stringify(prompts)}`);
// If no prompts, we need to return instead of attempting to ask prompts
if (!prompts.length) return;
debug('prompting the user - length > 0 for questions');
// Ask user for prompts
return this.prompt(prompts).then(props => {
// Convert user inputs to correct types
Object.entries(settings).forEach(([key, setting]) => {
switch ((setting.type || '').toLowerCase()) {
case 'number':
props[key] = Number(props[key]);
break;
case 'array':
case 'object':
if (props[key] == null || props[key] === '') {
delete props[key];
} else {
if (typeof props[key] === 'string') {
props[key] = JSON.parse(props[key]);
}
}
break;
}
});
this.artifactInfo = Object.assign(this.artifactInfo, {settings: props});
});
}
/**
* Scaffold DataSource related files
* super.scaffold() doesn't provide a way to rename files -- don't call it
*/
scaffold() {
// Exit if needed
if (this.shouldExit()) return false;
// Setting up data for templates
this.artifactInfo.className = utils.toClassName(this.artifactInfo.name);
this.artifactInfo.fileName = utils.toFileName(this.artifactInfo.name);
// prettier-ignore
this.artifactInfo.outFile = `${this.artifactInfo.fileName}.datasource.ts`;
// Resolved Output Paths.
const tsPath = this.destinationPath(
this.artifactInfo.outDir,
this.artifactInfo.outFile,
);
// template path
const classTemplatePath = this.templatePath('datasource.ts.ejs');
// Debug Info
debug(`this.artifactInfo.name => ${this.artifactInfo.name}`);
debug(`this.artifactInfo.className => ${this.artifactInfo.className}`);
debug(`this.artifactInfo.fileName => ${this.artifactInfo.fileName}`);
debug(`this.artifactInfo.outFile => ${this.artifactInfo.outFile}`);
debug(`tsPath => ${tsPath}`);
// Data to save to DataSource JSON file
const dsConfig = Object.assign(
{name: this.artifactInfo.name, connector: this.artifactInfo.connector},
this.artifactInfo.settings,
);
// From LB3
if (dsConfig.connector === 'ibm-object-storage') {
dsConfig.connector = 'loopback-component-storage';
dsConfig.provider = 'openstack';
dsConfig.useServiceCatalog = true;
dsConfig.useInternal = false;
dsConfig.keystoneAuthVersion = 'v3';
}
this.artifactInfo.dsConfigString = utils.stringifyObject(dsConfig, {
// Prevent inlining the config into a single line, e.g.
// const config = {name: 'db', connector: 'memory'};
inlineCharacterLimit: 0,
});
debug(`datasource configuration code: ${this.artifactInfo.dsConfigString}`);
this.copyTemplatedFiles(classTemplatePath, tsPath, this.artifactInfo);
}
install() {
if (this.shouldExit()) return false;
debug('install npm dependencies');
const pkgJson = this.packageJson || {};
const deps = pkgJson.get('dependencies') || {};
const pkgs = [];
// Connector package.
const connector = connectors[this.artifactInfo.connector];
if (connector && connector.package) {
if (!deps[connector.package.name]) {
pkgs.push(
connector.package.name +
`${
connector.package.version ? '@' + connector.package.version : ''
}`,
);
}
debug(`npmModule - ${pkgs[0]}`);
} else {
const connectorName = this.artifactInfo.connector;
// Other connectors that are not listed in `connectors.json`.
// No install is needed for those in connectors.json but without a
// package name as they are built-in connectors
if (!deps[connectorName] && !connector) pkgs.push(connectorName);
}
if (!deps['@loopback/repository']) {
pkgs.push('@loopback/repository');
}
if (pkgs.length === 0) return;
this.pkgManagerInstall(pkgs, {
npm: {
save: true,
},
});
}
async end() {
await super.end();
}
};