@loopback/cli
Version:
Yeoman generator for LoopBack 4
352 lines (299 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
'use strict';
const ArtifactGenerator = require('../../lib/artifact-generator');
const fs = require('fs');
const debug = require('../../lib/debug')('service-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 VALID_CONNECTORS_FOR_SERVICE = ['Model'];
const REMOTE_SERVICE_TEMPLATE = 'remote-service-proxy-template.ts.ejs';
const LOCAL_CLASS_TEMPLATE = 'local-service-class-template.ts.ejs';
const LOCAL_PROVIDER_TEMPLATE = 'local-service-provider-template.ts.ejs';
const PROMPT_MESSAGE_DATA_SOURCE = g.f('Please select the datasource');
const ERROR_NO_DATA_SOURCES_FOUND = g.f('No datasources found at');
const REMOTE_SERVICE_PROXY = 'proxy';
const LOCAL_SERVICE_CLASS = 'class';
const LOCAL_SERVICE_PROVIDER = 'provider';
const TEMPLATES = {
[REMOTE_SERVICE_PROXY]: REMOTE_SERVICE_TEMPLATE,
[LOCAL_SERVICE_CLASS]: LOCAL_CLASS_TEMPLATE,
[LOCAL_SERVICE_PROVIDER]: LOCAL_PROVIDER_TEMPLATE,
};
module.exports = class ServiceGenerator extends ArtifactGenerator {
// Note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);
}
_setupGenerator() {
this.artifactInfo = {
type: 'service',
rootDir: utils.sourceRootDir,
};
this.artifactInfo.outDir = path.resolve(
this.artifactInfo.rootDir,
utils.servicesDir,
);
this.artifactInfo.datasourcesDir = path.resolve(
this.artifactInfo.rootDir,
utils.datasourcesDir,
);
this.option('type', {
type: String,
required: false,
description: g.f('Service type - proxy, class or provider'),
});
this.option('datasource', {
type: String,
required: false,
description: g.f('A valid datasource name'),
});
return super._setupGenerator();
}
setOptions() {
return super.setOptions();
}
checkLoopBackProject() {
if (this.shouldExit()) return;
return super.checkLoopBackProject();
}
/**
* Ask for Service Type
*/
async promptServiceType() {
debug('Prompting for service type');
if (this.shouldExit()) return;
if (this.options.datasource) {
// The `datasource` option implies remote proxy
this.artifactInfo.serviceType = REMOTE_SERVICE_PROXY;
} else if (this.options.type === REMOTE_SERVICE_PROXY) {
this.artifactInfo.serviceType = REMOTE_SERVICE_PROXY;
} else if (this.options.type === LOCAL_SERVICE_CLASS) {
this.artifactInfo.serviceType = LOCAL_SERVICE_CLASS;
} else if (this.options.type === LOCAL_SERVICE_PROVIDER) {
this.artifactInfo.serviceType = LOCAL_SERVICE_PROVIDER;
} else if (this.options.type) {
this.exit(
new Error(
`Invalid service type: ${this.options.type} (${REMOTE_SERVICE_PROXY}, ${LOCAL_SERVICE_CLASS}, or ${LOCAL_SERVICE_PROVIDER}).`,
),
);
return;
}
if (!this.artifactInfo.serviceType) {
const prompts = [
{
type: 'list',
name: 'serviceType',
// capitalization
message: g.f('%s type:', utils.toClassName(this.artifactInfo.type)),
choices: [
{
name: g.f('Remote service proxy backed by a data source'),
value: REMOTE_SERVICE_PROXY,
},
{
name: g.f('Local service class bound to application context'),
value: LOCAL_SERVICE_CLASS,
},
{
name: g.f('Local service provider bound to application context'),
value: LOCAL_SERVICE_PROVIDER,
},
],
default: 0,
when: !this.options.datasource && !this.options.local,
},
];
const props = await this.prompt(prompts);
Object.assign(this.artifactInfo, props);
}
this.artifactInfo.serviceType =
this.artifactInfo.serviceType || REMOTE_SERVICE_PROXY;
this.artifactInfo.defaultTemplate =
TEMPLATES[this.artifactInfo.serviceType];
}
_isRemoteProxy() {
return this.artifactInfo.serviceType === REMOTE_SERVICE_PROXY;
}
async checkDataSources() {
if (this.shouldExit()) return;
if (!this._isRemoteProxy()) return;
// check for datasources
if (!fs.existsSync(this.artifactInfo.datasourcesDir)) {
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',
)}`,
);
}
}
async promptDataSourceName() {
debug('Prompting for a datasource ');
if (this.shouldExit()) return;
if (!this._isRemoteProxy()) return;
let datasourcesList;
// grab the datasource name 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(
`inspecting datasource: ${item} for basemodel: ${VALID_CONNECTORS_FOR_SERVICE}`,
);
const result = utils.isConnectorOfType(
VALID_CONNECTORS_FOR_SERVICE,
this.artifactInfo.datasourcesDir,
item,
);
return result;
});
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,
});
}
debug(`artifactInfo.dataSourceClass ${this.artifactInfo.dataSourceClass}`);
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);
this.artifactInfo.dataSourceName = utils.getDataSourceName(
this.artifactInfo.datasourcesDir,
this.artifactInfo.dataSourceClass,
);
if (!this.artifactInfo.dataSourceName) {
throw new Error('Datasource config does not have `name` property');
}
this.artifactInfo.dataSourceClassName =
utils.toClassName(this.artifactInfo.dataSourceName) + 'DataSource';
debug(`props after datasource prompt: ${inspect(props)}`);
return props;
})
.catch(err => {
debug(`Error during datasource prompt: ${err}`);
return this.exit(err);
});
}
/**
* Ask for Service Name
*/
async promptArtifactName() {
debug('Prompting for service name');
if (this.shouldExit()) return;
if (this.options.name) {
Object.assign(this.artifactInfo, {name: this.options.name});
}
const prompts = [
{
type: 'input',
name: 'name',
// capitalization
message: g.f('%s name:', utils.toClassName(this.artifactInfo.type)),
when: !this.artifactInfo.name,
validate: utils.validateClassName,
},
];
return this.prompt(prompts).then(props => {
Object.assign(this.artifactInfo, props);
return props;
});
}
/**
* this method will be in charge of inferring and create
* the remote interfaces from either SOAP wsdl or REST openApi json
*
* TODO: add functionality to inspect service specs to generate
* strongly-typed service proxies and corresponding model definitions.
*/
async inferInterfaces() {
if (this.shouldExit()) return;
if (!this._isRemoteProxy()) return;
const connectorType = utils.getDataSourceConnectorName(
this.artifactInfo.datasourcesDir,
this.artifactInfo.dataSourceClass,
);
// connectorType should output soap or rest in this case
// The base an further work for inferring remote methods
debug(`connectorType: ${connectorType}`);
}
scaffold() {
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);
Object.assign(this.artifactInfo, {
outFile: utils.getServiceFileName(this.artifactInfo.name),
});
const source = this.templatePath(this.artifactInfo.defaultTemplate);
const dest = this.destinationPath(
path.join(this.artifactInfo.outDir, this.artifactInfo.outFile),
);
debug(`artifactInfo: ${inspect(this.artifactInfo)}`);
debug(`Copying artifact to: ${dest}`);
this.copyTemplatedFiles(source, dest, this.artifactInfo);
return;
}
install() {
if (this.shouldExit()) return false;
if (!this._isRemoteProxy()) return;
this.artifactInfo.dataSourceClassName =
utils.toClassName(this.artifactInfo.dataSourceName) + 'DataSource';
debug('install npm dependencies');
const pkgJson = this.packageJson || {};
const deps = pkgJson.get('dependencies') || {};
const pkgs = [];
if (!deps['@loopback/service-proxy']) {
pkgs.push('@loopback/service-proxy');
}
if (pkgs.length === 0) return;
this.pkgManagerInstall(pkgs, {
npm: {
save: true,
},
});
}
async end() {
await super.end();
}
};