UNPKG

@loopback/cli

Version:
576 lines (505 loc) 17.5 kB
// 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 translate: Repository 'use strict'; const _ = require('lodash'); const ArtifactGenerator = require('../../lib/artifact-generator'); const fs = require('fs'); const debug = require('../../lib/debug')('repository-generator'); const inspect = require('util').inspect; const path = require('path'); const chalk = require('chalk'); const utils = require('../../lib/utils'); const tsquery = require('../../lib/ast-helper'); const g = require('../../lib/globalize'); const VALID_CONNECTORS_FOR_REPOSITORY = ['KeyValueModel', 'PersistedModel']; const KEY_VALUE_CONNECTOR = ['KeyValueModel']; const DEFAULT_CRUD_REPOSITORY = 'DefaultCrudRepository'; const KEY_VALUE_REPOSITORY = 'DefaultKeyValueRepository'; const BASE_REPOSITORIES = [DEFAULT_CRUD_REPOSITORY, KEY_VALUE_REPOSITORY]; const CLI_BASE_CRUD_REPOSITORIES = [ { name: `${DEFAULT_CRUD_REPOSITORY} ${chalk.gray('(Juggler bridge)')}`, value: DEFAULT_CRUD_REPOSITORY, }, ]; const CLI_BASE_KEY_VALUE_REPOSITORIES = [ { name: `${KEY_VALUE_REPOSITORY} ${chalk.gray( '(For access to a key-value store)', )}`, value: KEY_VALUE_REPOSITORY, }, ]; const CLI_BASE_SEPARATOR = [ { type: 'separator', line: g.f('----- Custom Repositories -----'), }, ]; const REPOSITORY_KV_TEMPLATE = 'repository-kv-template.ts.ejs'; const REPOSITORY_CRUD_TEMPLATE = 'repository-crud-default-template.ts.ejs'; const PROMPT_MESSAGE_MODEL = g.f( 'Select the model(s) you want to generate a repository for', ); const PROMPT_MESSAGE_DATA_SOURCE = g.f('Select the datasource'); const PROMPT_BASE_REPOSITORY_CLASS = g.f('Select the repository base class'); const ERROR_READING_FILE = g.f('Error reading file'); 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 RepositoryGenerator extends ArtifactGenerator { // Note: arguments and options should be defined in the constructor. constructor(args, opts) { super(args, opts); } /** * Find all the base artifacts in the given path whose type matches the * provided artifactType. * For example, a artifactType of "repository" will search the target path for * matches to "*.repository.base.ts" * @param {string} dir The target directory from which to load artifacts. * @param {string} artifactType The artifact type (ex. "model", "repository") */ async _findBaseClasses(dir, artifactType) { const paths = await utils.findArtifactPaths(dir, artifactType + '.base'); debug(`repository artifact paths: ${paths}`); // get base class and path const baseRepositoryList = []; for (const p of paths) { //get name removing anything from .artifactType.base const artifactFile = path.parse(_.last(_.split(p, path.sep))).name; const firstWord = _.first(_.split(artifactFile, '.')); const artifactName = utils.toClassName(firstWord) + utils.toClassName(artifactType); const baseRepository = {name: artifactName, file: artifactFile}; baseRepositoryList.push(baseRepository); } debug(`repository base classes: ${inspect(baseRepositoryList)}`); return baseRepositoryList; } /** * get the property name for the id field * @param {string} modelName */ async _getModelIdProperty(modelName) { let fileContent = ''; const modelFile = path.join( this.artifactInfo.modelDir, utils.getModelFileName(modelName), ); try { fileContent = this.fs.read(modelFile, {}); } catch (err) { debug(`${ERROR_READING_FILE} ${modelFile}: ${err.message}`); return this.exit(err); } return tsquery.getIdFromModel(fileContent); } /** * helper method to inspect and validate a repository type */ async _inferRepositoryType() { if (!this.artifactInfo.dataSourceClass) { return; } const result = utils.isConnectorOfType( KEY_VALUE_CONNECTOR, this.artifactInfo.datasourcesDir, this.artifactInfo.dataSourceClass, ); debug(`KeyValue Connector: ${result}`); if (result) { this.artifactInfo.repositoryTypeClass = KEY_VALUE_REPOSITORY; this.artifactInfo.defaultTemplate = REPOSITORY_KV_TEMPLATE; } else { this.artifactInfo.repositoryTypeClass = DEFAULT_CRUD_REPOSITORY; this.artifactInfo.defaultTemplate = REPOSITORY_CRUD_TEMPLATE; } this.artifactInfo.dataSourceName = utils.getDataSourceName( this.artifactInfo.datasourcesDir, this.artifactInfo.dataSourceClass, ); this.artifactInfo.dataSourceClassName = utils.toClassName(this.artifactInfo.dataSourceName) + 'DataSource'; } _setupGenerator() { this.artifactInfo = { type: 'repository ', rootDir: utils.sourceRootDir, }; this.artifactInfo.outDir = path.resolve( this.artifactInfo.rootDir, utils.repositoriesDir, ); this.artifactInfo.datasourcesDir = path.resolve( this.artifactInfo.rootDir, utils.datasourcesDir, ); this.artifactInfo.modelDir = path.resolve( this.artifactInfo.rootDir, utils.modelsDir, ); // to be able to write multiple files to the index.ts this.artifactInfo.indexesToBeUpdated = []; this.artifactInfo.defaultTemplate = REPOSITORY_CRUD_TEMPLATE; this.option('model', { type: String, required: false, description: g.f('A valid model name'), }); this.option('id', { type: String, required: false, description: g.f('A valid ID property name for the specified model'), }); this.option('datasource', { type: String, required: false, description: g.f('A valid datasource name'), }); this.option('repositoryBaseClass', { type: String, required: false, description: g.f('A valid repository base class'), }); 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 %j', cmdDatasourceName); try { datasourcesList = await utils.getArtifactList( this.artifactInfo.datasourcesDir, 'datasource', true, ); debug( 'datasourcesList from %s/%s:', utils.sourceRootDir, utils.datasourcesDir, datasourcesList, ); } catch (err) { return this.exit(err); } const availableDatasources = datasourcesList.filter(item => { const result = utils.isConnectorOfType( VALID_CONNECTORS_FOR_REPOSITORY, this.artifactInfo.datasourcesDir, item, ); debug( 'has %s connector of type %o? %s', item, VALID_CONNECTORS_FOR_REPOSITORY, result, ); 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._inferRepositoryType(); 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, 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); }); } async promptBaseClass() { debug('Prompting for repository base'); if (this.shouldExit()) return; const availableRepositoryList = []; debug(`repositoryTypeClass ${this.artifactInfo.repositoryTypeClass}`); // Add base repositories based on datasource type if (this.artifactInfo.repositoryTypeClass === KEY_VALUE_REPOSITORY) availableRepositoryList.push(...CLI_BASE_KEY_VALUE_REPOSITORIES); else availableRepositoryList.push(...CLI_BASE_CRUD_REPOSITORIES); availableRepositoryList.push(...CLI_BASE_SEPARATOR); try { this.artifactInfo.baseRepositoryList = await this._findBaseClasses( this.artifactInfo.outDir, 'repository', ); if ( this.artifactInfo.baseRepositoryList && this.artifactInfo.baseRepositoryList.length > 0 ) { availableRepositoryList.push(...this.artifactInfo.baseRepositoryList); debug(`availableRepositoryList ${availableRepositoryList}`); } } catch (err) { return this.exit(err); } this.artifactInfo.repositoryBaseClass = this.artifactInfo.repositoryTypeClass; if (this.options.repositoryBaseClass) { debug( `Base repository received from command line: ${this.options.repositoryBaseClass}`, ); this.artifactInfo.repositoryBaseClass = this.options.repositoryBaseClass; } return this.prompt([ { type: 'list', name: 'repositoryBaseClass', message: PROMPT_BASE_REPOSITORY_CLASS, choices: availableRepositoryList, when: this.artifactInfo.repositoryBaseClass === undefined || availableRepositoryList.length > 2, default: this.artifactInfo.repositoryBaseClass === undefined ? availableRepositoryList[0] : this.options.repositoryBaseClass, }, ]) .then(props => { debug(`props after custom repository prompt: ${inspect(props)}`); Object.assign(this.artifactInfo, props); return props; }) .catch(err => { debug(`Error during repository base class prompt: ${err.stack}`); return this.exit(err); }); } async promptModelId() { if (this.shouldExit()) return false; let idProperty; debug(`Model ID property name from command line: ${this.options.id}`); debug(`Selected Models: ${this.artifactInfo.modelNameList}`); if (_.isEmpty(this.artifactInfo.modelNameList)) { return this.exit(new Error(`${ERROR_NO_MODEL_SELECTED}`)); } else { // iterate thru each selected model, infer or ask for the ID type for (const item of this.artifactInfo.modelNameList) { this.artifactInfo.modelName = item; const prompts = [ { type: 'input', name: 'propertyName', message: g.f( 'Please enter the name of the ID property for %s:', `${item}`, ), default: 'id', }, ]; // user supplied the id from the command line if (this.options.id) { debug(`passing thru this.options.id with value : ${this.options.id}`); idProperty = this.options.id; /** make sure it is only used once, in case user selected more * than one model. */ delete this.options.id; } else { idProperty = await this._getModelIdProperty(item); if (idProperty === null) { const answer = await this.prompt(prompts); idProperty = answer.propertyName; } } this.artifactInfo.idProperty = idProperty; // Generate this repository await this._scaffold(); } } } async _scaffold() { if (this.shouldExit()) return false; this.artifactInfo.isRepositoryBaseBuiltin = BASE_REPOSITORIES.includes( this.artifactInfo.repositoryBaseClass, ); debug( `isRepositoryBaseBuiltin : ${this.artifactInfo.isRepositoryBaseBuiltin}`, ); if (!this.artifactInfo.isRepositoryBaseBuiltin) { const baseIndex = _.findIndex(this.artifactInfo.baseRepositoryList, [ 'name', this.artifactInfo.repositoryBaseClass, ]); this.artifactInfo.repositoryBaseFile = this.artifactInfo.baseRepositoryList[baseIndex].file; } if (this.options.name) { this.artifactInfo.className = utils.toClassName(this.options.name); this.artifactInfo.outFile = utils.getRepositoryFileName( this.options.name, ); // make sure the name supplied from cmd line is only used once delete this.options.name; } else { this.artifactInfo.className = utils.toClassName( this.artifactInfo.modelName, ); this.artifactInfo.outFile = utils.getRepositoryFileName( this.artifactInfo.modelName, ); this.artifactInfo.indexesToBeUpdated.push({ dir: this.artifactInfo.outDir, file: this.artifactInfo.outFile, }); } const source = this.templatePath( path.join( utils.sourceRootDir, utils.repositoriesDir, 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); return; } async end() { this.artifactInfo.type = this.artifactInfo.modelNameList && this.artifactInfo.modelNameList.length > 1 ? 'Repositories' : 'Repository'; this.artifactInfo.modelNameList = _.map( this.artifactInfo.modelNameList, repositoryName => { return repositoryName + 'Repository'; }, ); this.artifactInfo.name = `${this.artifactInfo.className}Repository`; await super.end(); } };