From 08797cc2c7cda688f729ddc4073c7c847821a41c Mon Sep 17 00:00:00 2001 From: Mario Estrada Date: Thu, 30 Aug 2018 09:38:00 -0600 Subject: [PATCH 1/6] feat(cli): add lb4 repository feature close #1588 --- packages/cli/generators/repository/index.js | 288 ++++++++++++++++++ .../repository-crud-default-template.ts.ejs | 12 + .../repository-kv-template.ts.ejs | 11 + packages/cli/lib/cli.js | 4 + 4 files changed, 315 insertions(+) create mode 100644 packages/cli/generators/repository/index.js create mode 100644 packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs create mode 100644 packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js new file mode 100644 index 000000000000..2d8e241f576f --- /dev/null +++ b/packages/cli/generators/repository/index.js @@ -0,0 +1,288 @@ +// Copyright IBM Corp. 2017,2018. 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 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 util = require('util'); +const fs = require('fs'); +const exists = util.promisify(fs.exists); + +const SERVICE_VALUE_CONNECTOR = 'soap,rest'; +const KEY_VALUE_CONNECTOR = 'kv-'; + +const KEY_VALUE_REPOSITORY = 'KeyValueRepository'; +const DEFAULT_CRUD_REPOSITORY = 'DefaultCrudRepository'; + +const REPOSITORY_KV_TEMPLATE = 'repository-kv-template.ts.ejs'; +const REPOSITORY_CRUD_TEMPLATE = 'repository-crud-default-template.ts.ejs'; + +const PROMPT_MESSAGE_MODEL = + 'Select the model you want to generate a repository'; +const PROMPT_MESSAGE_DATA_SOURCE = 'Please select the datasource'; +const PROMPT_MESSAGE_ID_TYPE = 'What is the type of your ID?'; + +const ERROR_READING_FILE = 'Error reading file'; +const ERROR_NO_DATA_SOURCES_FOUND = 'No datasources found in'; +const ERROR_NO_MODELS_FOUND = 'No models found in'; + +module.exports = class RepositoryGenerator extends ArtifactGenerator { + // Note: arguments and options should be defined in the constructor. + constructor(args, opts) { + super(args, opts); + + /** instance helper method isolated from the execution loop + * @connectorType: can be a single or a comma separated list + */ + this.isConnectorType = async function(connectorType, dataSourceClassName) { + debug(`callling isConnectorType ${connectorType}`); + let jsonFileContent = ''; + let result = false; + + let datasourceJSONFile = path.join( + 'src', + 'datasources', + dataSourceClassName + .replace('Datasource', '.datasource.json') + .toLowerCase(), + ); + + try { + const jsonFileExists = await exists(datasourceJSONFile); + if (jsonFileExists) { + jsonFileContent = this.fs.readJSON(datasourceJSONFile, {}); + } + } catch (err) { + debug(`${ERROR_READING_FILE} ${datasourceJSONFile}: ${err}`); + return this.exit(err); + } + + let keyWordsToSearch = connectorType.split(','); + for (let keyWord of keyWordsToSearch) { + debug(`asking for keyword ${keyWord}`); + if (jsonFileContent.connector.includes(keyWord)) { + result = true; + break; + } + } + + return result; + }; + } + + _setupGenerator() { + super._setupGenerator(); + + this.artifactInfo = { + type: 'repository', + rootDir: 'src', + }; + this.artifactInfo.outDir = path.resolve( + this.artifactInfo.rootDir, + 'repositories', + ); + this.artifactInfo.datasourcesDir = path.resolve( + this.artifactInfo.rootDir, + 'datasources', + ); + this.artifactInfo.modelDir = path.resolve( + this.artifactInfo.rootDir, + 'models', + ); + } + + setOptions() { + return super.setOptions(); + } + + checkLoopBackProject() { + return super.checkLoopBackProject(); + } + + async promptDataSource() { + debug('Prompting for a datasource '); + let datasourcesList; + + try { + datasourcesList = await utils.getArtifactList( + this.artifactInfo.datasourcesDir, + 'datasource', + true, + ); + } catch (err) { + return this.exit(err); + } + + // iterate over it to exclude service oriented data sources + let tempDataSourceList = Object.assign(datasourcesList, {}); + for (let item of tempDataSourceList) { + let result = await this.isConnectorType(SERVICE_VALUE_CONNECTOR, item); + debug(`${item} has keyword ${SERVICE_VALUE_CONNECTOR} is ${result}`); + if (result) { + // remove from original list + _.remove(datasourcesList, e => e == item); + } + } + + if (_.isEmpty(datasourcesList)) { + return this.exit( + `${ERROR_NO_DATA_SOURCES_FOUND} ${this.artifactInfo.datasourcesDir}. + ${chalk.yellow( + 'Please visit http://loopback.io/doc/en/lb4/Controller-generator.html for information on how repositories are discovered', + )}`, + ); + } + + return this.prompt([ + { + type: 'list', + name: 'dataSourceClassName', + message: PROMPT_MESSAGE_DATA_SOURCE, + choices: datasourcesList, + when: this.artifactInfo.dataSourceClassName === undefined, + default: datasourcesList[0], + validate: utils.validateClassName, + }, + ]) + .then(props => { + debug(`props: ${inspect(props)}`); + Object.assign(this.artifactInfo, props); + return props; + }) + .catch(err => { + debug(`Error during prompt for datasource name: ${err}`); + return this.exit(err); + }); + } + + async inferRepositoryType() { + let result = await this.isConnectorType( + KEY_VALUE_CONNECTOR, + this.artifactInfo.dataSourceClassName, + ); + + if (result) { + this.artifactInfo.repositoryTypeClass = KEY_VALUE_REPOSITORY; + } else { + this.artifactInfo.repositoryTypeClass = DEFAULT_CRUD_REPOSITORY; + } + + // assign the data source name to the information artifact + let dataSourceName = this.artifactInfo.dataSourceClassName + .replace('Datasource', '') + .toLowerCase(); + + Object.assign(this.artifactInfo, {dataSourceName: dataSourceName}); + // parent async end() checks for name property, albeit we don't use it here + Object.assign(this.artifactInfo, {name: dataSourceName}); + } + + async promptModels() { + let modelList; + try { + modelList = await utils.getArtifactList( + this.artifactInfo.modelDir, + 'model', + ); + } catch (err) { + return this.exit(err); + } + + if (_.isEmpty(modelList)) { + return this.exit( + `${ERROR_NO_MODELS_FOUND} ${this.artifactInfo.modelDir}. + ${chalk.yellow( + 'Please visit http://loopback.io/doc/en/lb4/Repository-generator.html for information on how models are discovered', + )}`, + ); + } + + return this.prompt([ + { + type: 'list', + name: 'modelName', + message: PROMPT_MESSAGE_MODEL, + choices: modelList, + when: this.artifactInfo.modelName === undefined, + default: modelList[0], + validate: utils.validateClassName, + }, + { + type: 'list', + name: 'idType', + message: PROMPT_MESSAGE_ID_TYPE, + choices: ['number', 'string', 'object'], + when: this.artifactInfo.idType === undefined, + default: 'number', + }, + ]) + .then(props => { + debug(`props: ${inspect(props)}`); + Object.assign(this.artifactInfo, props); + return props; + }) + .catch(err => { + debug(`Error during prompt for repository variables: ${err}`); + return this.exit(err); + }); + } + + scaffold() { + // We don't want to call the base scaffold function since it copies + // all of the templates! + if (this.shouldExit()) return false; + + this.artifactInfo.className = utils.toClassName(this.artifactInfo.name); + + this.artifactInfo.outFile = + utils.kebabCase(this.artifactInfo.modelName) + '.repository.ts'; + if (debug.enabled) { + debug(`Artifact output filename set to: ${this.artifactInfo.outFile}`); + } + + let template = ''; + + /* place a switch statement for future repository types */ + switch (this.artifactInfo.repositoryTypeClass) { + case KEY_VALUE_REPOSITORY: + template = REPOSITORY_KV_TEMPLATE; + break; + default: + template = REPOSITORY_CRUD_TEMPLATE; + } + + const source = this.templatePath( + path.join('src', 'repositories', template), + ); + if (debug.enabled) { + debug(`Using template at: ${source}`); + } + 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.fs.copyTpl( + source, + dest, + this.artifactInfo, + {}, + {globOptions: {dot: true}}, + ); + return; + } + + async end() { + await super.end(); + } +}; diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs new file mode 100644 index 000000000000..294ac8e0c535 --- /dev/null +++ b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs @@ -0,0 +1,12 @@ +import {<%= repositoryTypeClass %>}, juggler} from '@loopback/repository'; +import {<%= modelName %>} from '../models'; +import {inject} from '@loopback/core'; + +export class <%= className %>Repository extends <%= repositoryTypeClass %>< + <%= modelName %>, <%= idType %>> { + constructor( + @inject('datasources.<%= dataSourceName %>') protected datasource: juggler.DataSource, + ) { + super(<%= modelName %>, datasource); + } +} diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs new file mode 100644 index 000000000000..99497e5ee9d9 --- /dev/null +++ b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs @@ -0,0 +1,11 @@ +import {<%= repositoryTypeClass %>}} from '@loopback/repository'; +import {<%= modelName %>} from '../models'; +import {inject} from '@loopback/core'; + +export class <%= className %>Repository extends <%= repositoryTypeClass %><<%= modelName %>> { + constructor( + @inject('datasources.<%= dataSourceName %>') protected datasource: juggler.DataSource, + ) { + super(<%= modelName %>,datasource); + } +} diff --git a/packages/cli/lib/cli.js b/packages/cli/lib/cli.js index d92b600791c8..54273489b0ed 100644 --- a/packages/cli/lib/cli.js +++ b/packages/cli/lib/cli.js @@ -63,6 +63,10 @@ function setupGenerators() { PREFIX + 'datasource', ); env.register(path.join(__dirname, '../generators/model'), PREFIX + 'model'); + env.register( + path.join(__dirname, '../generators/repository'), + PREFIX + 'repository', + ); env.register( path.join(__dirname, '../generators/example'), PREFIX + 'example', From 2001ff65835cb613accf9b5191136ce4cd8d8616 Mon Sep 17 00:00:00 2001 From: Mario Estrada Date: Thu, 30 Aug 2018 10:37:49 -0600 Subject: [PATCH 2/6] feat(cli): ast-helper integration --- packages/cli/generators/repository/index.js | 398 +++++++++----- .../repository-crud-default-template.ts.ejs | 6 +- .../repository-kv-template.ts.ejs | 9 +- packages/cli/lib/ast-helper.js | 149 ++++++ packages/cli/package.json | 2 + .../test/integration/cli/cli.integration.js | 4 +- .../generators/repository.integration.js | 505 ++++++++++++++++++ 7 files changed, 946 insertions(+), 127 deletions(-) create mode 100644 packages/cli/lib/ast-helper.js create mode 100644 packages/cli/test/integration/generators/repository.integration.js diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js index 2d8e241f576f..547cc9e4664a 100644 --- a/packages/cli/generators/repository/index.js +++ b/packages/cli/generators/repository/index.js @@ -6,84 +6,144 @@ '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 util = require('util'); -const fs = require('fs'); -const exists = util.promisify(fs.exists); +const connectors = require('../datasource/connectors.json'); +const tsquery = require('../../lib/ast-helper'); -const SERVICE_VALUE_CONNECTOR = 'soap,rest'; -const KEY_VALUE_CONNECTOR = 'kv-'; +const VALID_CONNECTORS_FOR_REPOSITORY = ['KeyValueModel', 'PersistedModel']; +const KEY_VALUE_CONNECTOR = ['KeyValueModel']; -const KEY_VALUE_REPOSITORY = 'KeyValueRepository'; const DEFAULT_CRUD_REPOSITORY = 'DefaultCrudRepository'; +const KEY_VALUE_REPOSITORY = 'DefaultKeyValueRepository'; const REPOSITORY_KV_TEMPLATE = 'repository-kv-template.ts.ejs'; const REPOSITORY_CRUD_TEMPLATE = 'repository-crud-default-template.ts.ejs'; const PROMPT_MESSAGE_MODEL = - 'Select the model you want to generate a repository'; + 'Select the model(s) you want to generate a repository'; const PROMPT_MESSAGE_DATA_SOURCE = 'Please select the datasource'; -const PROMPT_MESSAGE_ID_TYPE = 'What is the type of your ID?'; - const ERROR_READING_FILE = 'Error reading file'; const ERROR_NO_DATA_SOURCES_FOUND = 'No datasources found in'; const ERROR_NO_MODELS_FOUND = 'No models found in'; +const ERROR_NO_MODEL_SELECTED = 'You did not select a valid model'; +const ERROR_NO_DIRECTORY = "couldn't find the directory"; module.exports = class RepositoryGenerator extends ArtifactGenerator { // Note: arguments and options should be defined in the constructor. constructor(args, opts) { super(args, opts); + } - /** instance helper method isolated from the execution loop - * @connectorType: can be a single or a comma separated list - */ - this.isConnectorType = async function(connectorType, dataSourceClassName) { - debug(`callling isConnectorType ${connectorType}`); - let jsonFileContent = ''; - let result = false; - - let datasourceJSONFile = path.join( - 'src', - 'datasources', - dataSourceClassName - .replace('Datasource', '.datasource.json') - .toLowerCase(), - ); + /** + * get the property name for the id field + * @param {string} modelName + */ + async _getModelIdType(modelName) { + let fileContent = ''; + let modelFile = path.join( + this.artifactInfo.modelDir, + `${utils.kebabCase(modelName)}.model.ts`, + ); + try { + fileContent = this.fs.read(modelFile, {}); + } catch (err) { + debug(`${ERROR_READING_FILE} ${modelFile}: ${err.message}`); + return this.exit(err); + } - try { - const jsonFileExists = await exists(datasourceJSONFile); - if (jsonFileExists) { - jsonFileContent = this.fs.readJSON(datasourceJSONFile, {}); - } - } catch (err) { - debug(`${ERROR_READING_FILE} ${datasourceJSONFile}: ${err}`); - return this.exit(err); - } + return tsquery.getIdFromModel(fileContent); + } - let keyWordsToSearch = connectorType.split(','); - for (let keyWord of keyWordsToSearch) { - debug(`asking for keyword ${keyWord}`); - if (jsonFileContent.connector.includes(keyWord)) { - result = true; - break; - } + /** + * helper method to inspect and validate a repository type + */ + async _inferRepositoryType() { + if (!this.artifactInfo.dataSourceClassName) { + return; + } + let result = this._isConnectorOfType( + KEY_VALUE_CONNECTOR, + this.artifactInfo.dataSourceClassName, + ); + 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; + } + + // assign the data source name to the information artifact + let dataSourceName = this.artifactInfo.dataSourceClassName + .replace('Datasource', '') + .toLowerCase(); + let dataSourceImportName = this.artifactInfo.dataSourceClassName.replace( + 'Datasource', + 'DataSource', + ); + + Object.assign(this.artifactInfo, { + dataSourceImportName: dataSourceImportName, + }); + Object.assign(this.artifactInfo, { + dataSourceName: dataSourceName, + }); + } + + /** + * load the connectors available and check if the basedModel matches any + * connectorType supplied for the given connector name + * @param {string} connectorType single or a comma separated string array + */ + _isConnectorOfType(connectorType, dataSourceClassName) { + debug(`calling isConnectorType ${connectorType}`); + let jsonFileContent = ''; + let result = false; + + if (!dataSourceClassName) { + return false; + } + let datasourceJSONFile = path.join( + this.artifactInfo.datasourcesDir, + dataSourceClassName + .replace('Datasource', '.datasource.json') + .toLowerCase(), + ); + + try { + jsonFileContent = this.fs.readJSON(datasourceJSONFile, {}); + } catch (err) { + debug(`${ERROR_READING_FILE} ${datasourceJSONFile}: ${err.message}`); + return this.exit(err); + } + + for (let connector of Object.values(connectors)) { + const matchedConnector = + jsonFileContent.connector === connector.name || + jsonFileContent.connector === `loopback-connector-${connector.name}`; + + if (matchedConnector && connectorType.includes(connector.baseModel)) { + result = true; + break; } + } - return result; - }; + return result; } _setupGenerator() { - super._setupGenerator(); - this.artifactInfo = { - type: 'repository', + type: 'datasource to use ', rootDir: 'src', }; + this.artifactInfo.outDir = path.resolve( this.artifactInfo.rootDir, 'repositories', @@ -96,47 +156,108 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { this.artifactInfo.rootDir, 'models', ); + + this.artifactInfo.defaultTemplate = REPOSITORY_CRUD_TEMPLATE; + + this.option('model', { + type: String, + required: false, + description: 'A valid model name', + }); + + this.option('id', { + type: String, + required: false, + description: 'A valid ID property name for the specified model', + }); + + return super._setupGenerator(); } setOptions() { return super.setOptions(); } - checkLoopBackProject() { - return super.checkLoopBackProject(); + async checkPaths() { + // check for datasources + if (!fs.existsSync(this.artifactInfo.datasourcesDir)) { + return this.exit( + new Error( + `${ERROR_NO_DIRECTORY} ${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_DIRECTORY} ${this.artifactInfo.modelDir}. + ${chalk.yellow( + 'Please visit https://loopback.io/doc/en/lb4/Model-generator.html for information on how models are discovered', + )}`, + ), + ); + } } - async promptDataSource() { + async promptDataSourceName() { + if (this.shouldExit()) return false; + debug('Prompting for a datasource '); + let cmdDatasourceName; let datasourcesList; + // grab the datasourcename from the command line + cmdDatasourceName = this.options.name + ? utils.toClassName(this.options.name) + 'Datasource' + : ''; + + debug(`command line datasource is ${cmdDatasourceName}`); + try { datasourcesList = await utils.getArtifactList( this.artifactInfo.datasourcesDir, 'datasource', true, ); + debug(`datasourcesList from src/datasources : ${datasourcesList}`); } catch (err) { return this.exit(err); } - // iterate over it to exclude service oriented data sources - let tempDataSourceList = Object.assign(datasourcesList, {}); - for (let item of tempDataSourceList) { - let result = await this.isConnectorType(SERVICE_VALUE_CONNECTOR, item); - debug(`${item} has keyword ${SERVICE_VALUE_CONNECTOR} is ${result}`); - if (result) { - // remove from original list - _.remove(datasourcesList, e => e == item); - } + const availableDatasources = datasourcesList.filter(item => { + debug(`data source unfiltered list: ${item}`); + const result = this._isConnectorOfType( + VALID_CONNECTORS_FOR_REPOSITORY, + item, + ); + return result; + }); + + if (availableDatasources.includes(cmdDatasourceName)) { + Object.assign(this.artifactInfo, { + dataSourceClassName: cmdDatasourceName, + }); } - if (_.isEmpty(datasourcesList)) { + debug( + `artifactInfo.dataSourceClassName ${ + this.artifactInfo.dataSourceClassName + }`, + ); + + if (availableDatasources.length === 0) { return this.exit( - `${ERROR_NO_DATA_SOURCES_FOUND} ${this.artifactInfo.datasourcesDir}. + new Error( + `${ERROR_NO_DATA_SOURCES_FOUND} ${this.artifactInfo.datasourcesDir}. ${chalk.yellow( - 'Please visit http://loopback.io/doc/en/lb4/Controller-generator.html for information on how repositories are discovered', + 'Please visit https://loopback.io/doc/en/lb4/DataSource-generator.html for information on how datasources are discovered', )}`, + ), ); } @@ -145,48 +266,31 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { type: 'list', name: 'dataSourceClassName', message: PROMPT_MESSAGE_DATA_SOURCE, - choices: datasourcesList, - when: this.artifactInfo.dataSourceClassName === undefined, - default: datasourcesList[0], + choices: availableDatasources, + when: !this.artifactInfo.dataSourceClassName, + default: availableDatasources[0], validate: utils.validateClassName, }, ]) .then(props => { - debug(`props: ${inspect(props)}`); Object.assign(this.artifactInfo, props); + debug(`props after datasource prompt: ${inspect(props)}`); return props; }) .catch(err => { - debug(`Error during prompt for datasource name: ${err}`); + debug(`Error during datasource prompt: ${err}`); return this.exit(err); }); } - async inferRepositoryType() { - let result = await this.isConnectorType( - KEY_VALUE_CONNECTOR, - this.artifactInfo.dataSourceClassName, - ); - - if (result) { - this.artifactInfo.repositoryTypeClass = KEY_VALUE_REPOSITORY; - } else { - this.artifactInfo.repositoryTypeClass = DEFAULT_CRUD_REPOSITORY; - } - - // assign the data source name to the information artifact - let dataSourceName = this.artifactInfo.dataSourceClassName - .replace('Datasource', '') - .toLowerCase(); + async promptModels() { + if (this.shouldExit()) return false; - Object.assign(this.artifactInfo, {dataSourceName: dataSourceName}); - // parent async end() checks for name property, albeit we don't use it here - Object.assign(this.artifactInfo, {name: dataSourceName}); - } + await this._inferRepositoryType(); - async promptModels() { let modelList; try { + debug(`model list dir ${this.artifactInfo.modelDir}`); modelList = await utils.getArtifactList( this.artifactInfo.modelDir, 'model', @@ -195,72 +299,116 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { return this.exit(err); } - if (_.isEmpty(modelList)) { + 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.length > 0 && modelList.includes(this.options.model)) { + Object.assign(this.artifactInfo, {modelNameList: [this.options.model]}); + } else { + modelList = []; + } + } + + if (modelList.length === 0) { return this.exit( - `${ERROR_NO_MODELS_FOUND} ${this.artifactInfo.modelDir}. + new Error( + `${ERROR_NO_MODELS_FOUND} ${this.artifactInfo.modelDir}. ${chalk.yellow( - 'Please visit http://loopback.io/doc/en/lb4/Repository-generator.html for information on how models are discovered', + 'Please visit https://loopback.io/doc/en/lb4/Model-generator.html for information on how models are discovered', )}`, + ), ); } return this.prompt([ { - type: 'list', - name: 'modelName', + type: 'checkbox', + name: 'modelNameList', message: PROMPT_MESSAGE_MODEL, choices: modelList, - when: this.artifactInfo.modelName === undefined, - default: modelList[0], - validate: utils.validateClassName, - }, - { - type: 'list', - name: 'idType', - message: PROMPT_MESSAGE_ID_TYPE, - choices: ['number', 'string', 'object'], - when: this.artifactInfo.idType === undefined, - default: 'number', + when: this.artifactInfo.modelNameList === undefined, }, ]) .then(props => { - debug(`props: ${inspect(props)}`); Object.assign(this.artifactInfo, props); + debug(`props after model list prompt: ${inspect(props)}`); return props; }) .catch(err => { - debug(`Error during prompt for repository variables: ${err}`); + debug(`Error during model list prompt: ${err}`); return this.exit(err); }); } - scaffold() { - // We don't want to call the base scaffold function since it copies - // all of the templates! + async promptModelId() { + if (this.shouldExit()) return false; + + 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 (let item of this.artifactInfo.modelNameList) { + this.artifactInfo.modelName = item; + + const prompts = [ + { + type: 'input', + name: 'propertyName', + message: `Please enter the name of the ID property for ${item}:`, + default: 'id', + }, + ]; + + let idType = await this._getModelIdType(item); + + /** + * fallback by asking the user for ID property name if the user + * didn't supplied any from the command line. If we inferred it, then + * the supplied argument is ignored + */ + if (idType === null) { + if (this.options.id) { + idType = this.options.id; + /** make sure it is only used once, in case user selected more + * than one model. + */ + delete this.options.id; + } else { + const answer = await this.prompt(prompts); + idType = answer.propertyName; + } + } + + this.artifactInfo.idType = idType; + // Generate this repository + await this._scaffold(); + } + } + } + + async _scaffold() { if (this.shouldExit()) return false; - this.artifactInfo.className = utils.toClassName(this.artifactInfo.name); + this.artifactInfo.className = utils.toClassName( + this.artifactInfo.modelName, + ); this.artifactInfo.outFile = utils.kebabCase(this.artifactInfo.modelName) + '.repository.ts'; + if (debug.enabled) { debug(`Artifact output filename set to: ${this.artifactInfo.outFile}`); } - let template = ''; - - /* place a switch statement for future repository types */ - switch (this.artifactInfo.repositoryTypeClass) { - case KEY_VALUE_REPOSITORY: - template = REPOSITORY_KV_TEMPLATE; - break; - default: - template = REPOSITORY_CRUD_TEMPLATE; - } - const source = this.templatePath( - path.join('src', 'repositories', template), + path.join('src', 'repositories', this.artifactInfo.defaultTemplate), ); + if (debug.enabled) { debug(`Using template at: ${source}`); } @@ -283,6 +431,16 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { } async end() { + this.artifactInfo.type = + this.artifactInfo.modelNameList && + this.artifactInfo.modelNameList.length > 1 + ? 'Repositories' + : 'Repository'; + + this.artifactInfo.name = this.artifactInfo.modelNameList + ? this.artifactInfo.modelNameList.join() + : this.artifactInfo.modelName; + await super.end(); } }; diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs index 294ac8e0c535..2ed7c98b06da 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs @@ -1,9 +1,11 @@ -import {<%= repositoryTypeClass %>}, juggler} from '@loopback/repository'; +import {<%= repositoryTypeClass %>, juggler} from '@loopback/repository'; import {<%= modelName %>} from '../models'; import {inject} from '@loopback/core'; export class <%= className %>Repository extends <%= repositoryTypeClass %>< - <%= modelName %>, <%= idType %>> { + <%= modelName %>, + typeof <%= modelName %>.prototype.<%= idType %> +> { constructor( @inject('datasources.<%= dataSourceName %>') protected datasource: juggler.DataSource, ) { diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs index 99497e5ee9d9..5d5ffc251b87 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs @@ -1,10 +1,13 @@ -import {<%= repositoryTypeClass %>}} from '@loopback/repository'; +import {<%= repositoryTypeClass %>} from '@loopback/repository'; import {<%= modelName %>} from '../models'; +import {<%= dataSourceImportName %>} from '../datasources'; import {inject} from '@loopback/core'; -export class <%= className %>Repository extends <%= repositoryTypeClass %><<%= modelName %>> { +export class <%= className %>Repository extends <%= repositoryTypeClass %>< + <%= modelName %> + > { constructor( - @inject('datasources.<%= dataSourceName %>') protected datasource: juggler.DataSource, + @inject('datasources.<%= dataSourceName %>') datasource: <%= dataSourceImportName %>, ) { super(<%= modelName %>,datasource); } diff --git a/packages/cli/lib/ast-helper.js b/packages/cli/lib/ast-helper.js new file mode 100644 index 000000000000..6cda7d20054d --- /dev/null +++ b/packages/cli/lib/ast-helper.js @@ -0,0 +1,149 @@ +// Copyright IBM Corp. 2017,2018. 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 tsquery = require('@phenomnomnominal/tsquery').tsquery; +const debug = require('./debug')('ast-query'); + +const tsArtifact = { + ClassDeclaration: 'ClassDeclaration', + PropertyDeclaration: 'PropertyDeclaration', + Identifier: 'Identifier', + Decorator: 'Decorator', + CallExpression: 'CallExpression', + ObjectLiteralExpression: 'ObjectLiteralExpression', + PropertyAssignment: 'PropertyAssignment', + PropertyDeclaration: 'PropertyDeclaration', + TrueKeywordTrue: 'TrueKeyword[value="true"]', + // the following are placed in case it is needed to explore more artifacts + IfStatement: 'IfStatement', + ForStatement: 'ForStatement', + WhileStatement: 'WhileStatement', + DoStatement: 'DoStatement', + VariableStatement: 'VariableStatement', + FunctionDeclaration: 'FunctionDeclaration', + ArrowFunction: 'ArrowFunction', + ImportDeclaration: 'ImportDeclaration', + StringLiteral: 'StringLiteral', + FalseKeyword: 'FalseKeyword', + NullKeyword: 'NullKeyword', + AnyKeyword: 'AnyKeyword', + NumericLiteral: 'NumericLiteral', + NoSubstitutionTemplateLiteral: 'NoSubstitutionTemplateLiteral', + TemplateExpression: 'TemplateExpression', +}; + +const RootNodesFindID = [ + // Defaul format generated by lb4 model + [ + tsArtifact.ClassDeclaration, + tsArtifact.PropertyDeclaration, + tsArtifact.Identifier, + ], + // Model JSON definition inside the @model decorator + [ + tsArtifact.ClassDeclaration, + tsArtifact.Decorator, + tsArtifact.CallExpression, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.Identifier, + ], + // Model JSON static definition inside the Class + [ + tsArtifact.ClassDeclaration, + tsArtifact.PropertyDeclaration, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.Identifier, + ], +]; +const ChildNodesFindID = [ + // Defaul format generated by lb4 model + [ + tsArtifact.ClassDeclaration, + tsArtifact.PropertyDeclaration, + tsArtifact.Decorator, + tsArtifact.CallExpression, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.TrueKeywordTrue, + ], + + // Model JSON definition inside the @model decorator + [ + tsArtifact.ClassDeclaration, + tsArtifact.Decorator, + tsArtifact.CallExpression, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.TrueKeywordTrue, + ], + // Model JSON static definition inside the Class + [ + tsArtifact.ClassDeclaration, + tsArtifact.PropertyDeclaration, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.ObjectLiteralExpression, + tsArtifact.PropertyAssignment, + tsArtifact.TrueKeywordTrue, + ], +]; + +/** + * Parse the file using the possible formats specified in the arrays + * rootNodesFindID and childNodesFindID + * @param {string} fileContent with a model.ts class + */ +exports.getIdFromModel = async function(fileContent) { + let nodePos = 0; + let retVal = null; + + const ast = tsquery.ast(fileContent); + for (let rootNodes of RootNodesFindID) { + const propertyArr = []; + const stRootNode = rootNodes.join('>'); + const nodes = tsquery(ast, stRootNode); + + debug(`rootNode ${stRootNode}`); + + for (let a of nodes) { + propertyArr.push(a.escapedText); + } + + const stChildNode = ChildNodesFindID[nodePos].join('>'); + const subnodes = tsquery(ast, stChildNode); + + let i = 0; + for (let a of subnodes) { + if (a.parent.name.escapedText === 'id') { + // we found the primary key for the model + retVal = propertyArr[i]; + debug(`found key: ${retVal}`); + break; + } + i++; + } + + if (retVal !== null) { + break; + } + + nodePos++; + } + + return retVal; +}; diff --git a/packages/cli/package.json b/packages/cli/package.json index 76034da7bae7..c02d5fd0fea1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,12 +36,14 @@ "request-promise-native": "^1.0.5", "rimraf": "^2.6.2", "sinon": "^4.5.0", + "typescript": "^2.9.2", "yeoman-assert": "^3.1.1", "yeoman-environment": "^2.0.6", "yeoman-test": "^1.7.0" }, "dependencies": { "@loopback/dist-util": "^0.3.7", + "@phenomnomnominal/tsquery": "^2.0.1", "camelcase-keys": "^4.2.0", "chalk": "^2.3.2", "change-case": "^3.0.2", diff --git a/packages/cli/test/integration/cli/cli.integration.js b/packages/cli/test/integration/cli/cli.integration.js index 1f9347bd578d..ee6fa01ea1ab 100644 --- a/packages/cli/test/integration/cli/cli.integration.js +++ b/packages/cli/test/integration/cli/cli.integration.js @@ -24,7 +24,7 @@ describe('cli', () => { expect(entries).to.eql([ 'Available commands: ', ' lb4 app\n lb4 extension\n lb4 controller\n lb4 datasource\n ' + - 'lb4 model\n lb4 example\n lb4 openapi', + 'lb4 model\n lb4 repository\n lb4 example\n lb4 openapi', ]); }); @@ -42,7 +42,7 @@ describe('cli', () => { expect(entries).to.containEql('Available commands: '); expect(entries).to.containEql( ' lb4 app\n lb4 extension\n lb4 controller\n lb4 datasource\n ' + - 'lb4 model\n lb4 example\n lb4 openapi', + 'lb4 model\n lb4 repository\n lb4 example\n lb4 openapi', ); }); diff --git a/packages/cli/test/integration/generators/repository.integration.js b/packages/cli/test/integration/generators/repository.integration.js new file mode 100644 index 000000000000..11aa4cfc486a --- /dev/null +++ b/packages/cli/test/integration/generators/repository.integration.js @@ -0,0 +1,505 @@ +// Copyright IBM Corp. 2018. 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 path = require('path'); +const assert = require('yeoman-assert'); +const testlab = require('@loopback/testlab'); +const fs = require('fs'); + +const expect = testlab.expect; +const TestSandbox = testlab.TestSandbox; + +const generator = path.join(__dirname, '../../../generators/repository'); + +const testUtils = require('../../test-utils'); + +// Test Sandbox +const SANDBOX_PATH = path.resolve(__dirname, '..', '.sandbox'); +const sandbox = new TestSandbox(SANDBOX_PATH); + +describe('lb4 repository', () => { + beforeEach('reset sandbox', async () => { + await sandbox.reset(); + }); + + // special cases regardless of the repository type + describe('generate repositories on special conditions', () => { + it('generates a multi-word crud repository', async () => { + const multiItemPrompt = { + dataSourceClassName: 'DbmemDatasource', + modelNameList: ['MultiWord'], + }; + + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withPrompts(multiItemPrompt); + + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'multi-word.repository.ts', + ); + + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /export class MultiWordRepository extends DefaultCrudRepository { + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withArguments('--config myconfig.json'); + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'decoratordefined.repository.ts', + ); + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /export class DecoratordefinedRepository extends DefaultCrudRepository\ { + const multiItemPrompt = { + dataSourceClassName: 'DbmemDatasource', + modelNameList: ['InvalidId'], + propertyName: 'myid', + }; + + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withPrompts(multiItemPrompt); + + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'invalid-id.repository.ts', + ); + + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /export class InvalidIdRepository extends DefaultCrudRepository { + it('does not run with an invalid model name', async () => { + const basicPrompt = { + dataSourceClassName: 'DbmemDatasource', + }; + return expect( + testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withPrompts(basicPrompt) + .withArguments(' --model InvalidModel'), + ).to.be.rejectedWith(/No models found/); + }); + + it("does not run when user doesn't select a model", async () => { + const basicPrompt = { + dataSourceClassName: 'DbmemDatasource', + }; + return expect( + testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withPrompts(basicPrompt), + ).to.be.rejectedWith(/You did not select a valid model/); + }); + + it('does not run with empty datasource list', async () => { + return expect( + testUtils.executeGenerator(generator).inDir( + SANDBOX_PATH, + async () => + await prepareGeneratorForRepository(SANDBOX_PATH, { + noFixtures: true, + }), + ), + ).to.be.rejectedWith(/No datasources found/); + }); + }); + + describe('valid generation of crud repositories', () => { + it('generates a crud repository from default model', async () => { + const basicPrompt = { + dataSourceClassName: 'DbmemDatasource', + }; + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withPrompts(basicPrompt) + .withArguments(' --model Defaultmodel'); + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'defaultmodel.repository.ts', + ); + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /export class DefaultmodelRepository extends DefaultCrudRepository\ { + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withArguments('dbmem --model decoratordefined'); + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'decoratordefined.repository.ts', + ); + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /export class DecoratordefinedRepository extends DefaultCrudRepository\ { + it('generates a kv repository from default model', async () => { + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withArguments('dbkv --model Defaultmodel'); + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'defaultmodel.repository.ts', + ); + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /DefaultmodelRepository extends DefaultKeyValueRepository { + const basicPrompt = { + dataSourceClassName: 'DbkvDatasource', + }; + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withPrompts(basicPrompt) + .withArguments('--model decoratordefined'); + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'decoratordefined.repository.ts', + ); + + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /DecoratordefinedRepository extends DefaultKeyValueRepository) { + super(data); + } + } + `, + }, + { + path: MODEL_APP_PATH, + file: 'defaultmodel.model.ts', + content: ` + import {Entity, model, property} from '@loopback/repository'; + + @model() + export class DefaultModel extends Entity { + @property({ + type: 'number', + id: true, + default: 0, + }) + id?: number; + + @property({ + type: 'string', + }) + desc?: string; + + @property({ + type: 'number', + default: 0, + }) + balance?: number; + + constructor(data?: Partial) { + super(data); + } + } + `, + }, + { + path: MODEL_APP_PATH, + file: 'multi-word.model.ts', + content: ` + import {Entity, model, property} from '@loopback/repository'; + + @model() + export class MultiWord extends Entity { + @property({ + type: 'string', + id: true, + default: 0, + }) + pk?: string; + + @property({ + type: 'string', + }) + desc?: string; + + constructor(data?: Partial) { + super(data); + } + } + `, + }, + { + path: MODEL_APP_PATH, + file: 'invalid-id.model.ts', + content: ` + import {Entity, model, property} from '@loopback/repository'; + + @model() + export class InvalidID extends Entity { + @property({ + type: 'string', + required: true, + default: 0, + }) + id: string; + + @property({ + type: 'string', + }) + desc?: string; + + constructor(data?: Partial) { + super(data); + } + } + `, + }, +]; + +async function prepareGeneratorForRepository(rootDir, options) { + options = options || {}; + const content = {}; + if (!options.excludeKeyword) { + content.keywords = ['loopback']; + } + + if (!options.excludePackageJSON) { + fs.writeFileSync( + path.join(rootDir, 'package.json'), + JSON.stringify(content), + ); + } + + if (!options.excludeYoRcJSON) { + fs.writeFileSync(path.join(rootDir, '.yo-rc.json'), JSON.stringify({})); + } + + fs.mkdirSync(path.join(rootDir, 'src')); + + if (!options.excludeControllersDir) { + fs.mkdirSync(path.join(rootDir, 'src', 'controllers')); + } + + if (!options.excludeModelsDir) { + fs.mkdirSync(path.join(rootDir, 'src', 'models')); + } + + if (!options.excludeRepositoriesDir) { + fs.mkdirSync(path.join(rootDir, 'src', 'repositories')); + } + + if (!options.excludeDataSourcesDir) { + fs.mkdirSync(path.join(rootDir, 'src', 'datasources')); + } + + if (!options.noFixtures) { + copyFixtures(); + } +} + +function copyFixtures() { + for (let theFile of SANDBOX_FILES) { + const fullPath = path.join(SANDBOX_PATH, theFile.path, theFile.file); + if (!fs.existsSync(fullPath)) { + fs.writeFileSync(fullPath, theFile.content); + } + } +} From 499d31a17203ec3b25212bb5701cea142d6887bb Mon Sep 17 00:00:00 2001 From: Mario Estrada Date: Thu, 13 Sep 2018 20:46:22 -0600 Subject: [PATCH 3/6] docs(cli): add documentation for lb4 repository --- docs/site/Repository-generator.md | 97 +++++++++++++++++++ docs/site/sidebars/lb4_sidebar.yml | 4 + docs/site/tables/lb4-artifact-commands.html | 6 ++ packages/cli/generators/repository/index.js | 61 +++++++----- .../generators/repository.integration.js | 30 +++++- 5 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 docs/site/Repository-generator.md diff --git a/docs/site/Repository-generator.md b/docs/site/Repository-generator.md new file mode 100644 index 000000000000..ce835328defc --- /dev/null +++ b/docs/site/Repository-generator.md @@ -0,0 +1,97 @@ +--- +lang: en +title: 'Repository generator' +keywords: LoopBack 4.0, LoopBack 4 +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Repository-generator.html +--- + +{% include content/generator-create-app.html lang=page.lang %} + +### Synopsis + +Adds a new [Repository or Multiple Repositories](Repositories.md) class to a +LoopBack application with one single command. + +```sh +lb4 repository [options] [] +``` + +### Options + +`--datasource` : _(Optional)_ name of a valid datasource already created in +src/datasources + +`--model` : _(Optional)_ name of a valid model already created in src/models + +`--id` : _(Optional)_ name of the property serving as **ID** in the selected +model. If you supply this value, the CLI will not try to infer this value from +the selected model file. + +### Configuration file + +This generator supports a config file with the following format, see the +Standard options below to see different ways you can supply this configuration +file. + +```ts +{ + "name": "repositoryNameToBeGenerated", + "datasource": "validDataSourceName", + "model": "validDModelName", + "id": "anOptionalNameForID" +} +``` + +### Notes + +Service oriented datasources such as REST or SOAP are not considered valid and +thus not presented to you in the selection list. + +There should be at least one valid _(KeyValue or Persisted)_ data source and one +model already created in their respective directories. + +{% include_relative includes/CLI-std-options.md %} + +### Arguments + +`` - Optional argument specifyng the respository name to be generated. In +case you select multiple models, the first model will take this argument for its +repository file name. + +### Interactive Prompts + +The tool will prompt you for: + +- **Please select the datasource.** _(name)_ If the name of the datasource had + been supplied from the command line, the prompt is skipped, otherwise it will + present you, the list of available datasources to select one. It will use this + datasource to check what kind of repository it will generate. + +- **Select the model(s) you want to generate a repository.** _(model)_ If the + name of the model had been supplied from the command line with `--model` + option and it is a valid model, the the prompt is skipped, otherwise it will + present the error `Error: No models found` in the console. + + If no `--model` is supplied, then the it will present you with a valid list of + models from `src/models` directory and you will be able to select one or + multiple models. The tool will generate a repository for each of the selected + models. + + **NOTE:** The tool will inspect each of the selected models and try to find + the name of the property serving as **ID** for the model. + +- **Please enter the name of the ID property for _modelName_.** _(id)_ If the + CLI cannot find the corresponding ID property name for the model, it will + prompt you to enter a name here. If you don't specify any name, it will use + _id_ as the default one. + +### Output + +Once all the prompts have been answered, the CLI will do the following for each +of the selected models. + +- Create a Repository class as follows: + `/src/repositories/${modelName}.repository.ts` +- Update `/src/repositories/index.ts` to export the newly created Repository + class. diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index a6404836bbf1..a2cb158f55a6 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -223,6 +223,10 @@ children: url: Model-generator.html output: 'web, pdf' + - title: 'Repository generator' + url: Repository-generator.html + output: 'web, pdf' + - title: 'OpenAPI generator' url: OpenAPI-generator.html output: 'web, pdf' diff --git a/docs/site/tables/lb4-artifact-commands.html b/docs/site/tables/lb4-artifact-commands.html index 0454b758fb78..2848d5eedd2d 100644 --- a/docs/site/tables/lb4-artifact-commands.html +++ b/docs/site/tables/lb4-artifact-commands.html @@ -29,6 +29,12 @@ Model generator + + lb4 repository + Add a new repository or multiple repositories to a LoopBack 4 application + Repository generator + + lb4 openapi Generate controllers and models from OpenAPI specs diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js index 547cc9e4664a..49498d048bf2 100644 --- a/packages/cli/generators/repository/index.js +++ b/packages/cli/generators/repository/index.js @@ -31,7 +31,7 @@ const ERROR_READING_FILE = 'Error reading file'; const ERROR_NO_DATA_SOURCES_FOUND = 'No datasources found in'; const ERROR_NO_MODELS_FOUND = 'No models found in'; const ERROR_NO_MODEL_SELECTED = 'You did not select a valid model'; -const ERROR_NO_DIRECTORY = "couldn't find the directory"; +const ERROR_NO_DIRECTORY = 'The directory was not found'; module.exports = class RepositoryGenerator extends ArtifactGenerator { // Note: arguments and options should be defined in the constructor. @@ -140,7 +140,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { _setupGenerator() { this.artifactInfo = { - type: 'datasource to use ', + type: 'repository ', rootDir: 'src', }; @@ -171,6 +171,12 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { description: 'A valid ID property name for the specified model', }); + this.option('datasource', { + type: String, + required: false, + description: 'A valid datasource name', + }); + return super._setupGenerator(); } @@ -212,8 +218,8 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { let datasourcesList; // grab the datasourcename from the command line - cmdDatasourceName = this.options.name - ? utils.toClassName(this.options.name) + 'Datasource' + cmdDatasourceName = this.options.datasource + ? utils.toClassName(this.options.datasource) + 'Datasource' : ''; debug(`command line datasource is ${cmdDatasourceName}`); @@ -344,6 +350,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { async promptModelId() { if (this.shouldExit()) return false; + let idType; debug(`Model ID property name from command line: ${this.options.id}`); debug(`Selected Models: ${this.artifactInfo.modelNameList}`); @@ -364,26 +371,22 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { }, ]; - let idType = await this._getModelIdType(item); - - /** - * fallback by asking the user for ID property name if the user - * didn't supplied any from the command line. If we inferred it, then - * the supplied argument is ignored - */ - if (idType === null) { - if (this.options.id) { - idType = this.options.id; - /** make sure it is only used once, in case user selected more - * than one model. - */ - delete this.options.id; - } else { + // user supplied the id from the command line + if (this.options.id) { + debug(`passing thru this.options.id with value : ${this.options.id}`); + + idType = this.options.id; + /** make sure it is only used once, in case user selected more + * than one model. + */ + delete this.options.id; + } else { + idType = await this._getModelIdType(item); + if (idType === null) { const answer = await this.prompt(prompts); idType = answer.propertyName; } } - this.artifactInfo.idType = idType; // Generate this repository await this._scaffold(); @@ -394,12 +397,20 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { async _scaffold() { if (this.shouldExit()) return false; - this.artifactInfo.className = utils.toClassName( - this.artifactInfo.modelName, - ); + if (this.options.name) { + this.artifactInfo.className = utils.toClassName(this.options.name); + this.artifactInfo.outFile = + utils.kebabCase(this.options.name) + '.repository.ts'; - this.artifactInfo.outFile = - utils.kebabCase(this.artifactInfo.modelName) + '.repository.ts'; + // 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.kebabCase(this.artifactInfo.modelName) + '.repository.ts'; + } if (debug.enabled) { debug(`Artifact output filename set to: ${this.artifactInfo.outFile}`); diff --git a/packages/cli/test/integration/generators/repository.integration.js b/packages/cli/test/integration/generators/repository.integration.js index 11aa4cfc486a..5984f4ec2ecc 100644 --- a/packages/cli/test/integration/generators/repository.integration.js +++ b/packages/cli/test/integration/generators/repository.integration.js @@ -61,6 +61,30 @@ describe('lb4 repository', () => { ); }); + it('generates a custom name repository', async () => { + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withArguments('myrepo --datasource dbmem --model MultiWord'); + const expectedFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'myrepo.repository.ts', + ); + + assert.file(expectedFile); + assert.fileContent( + expectedFile, + /export class MyrepoRepository extends DefaultCrudRepository { await testUtils .executeGenerator(generator) @@ -208,7 +232,7 @@ describe('lb4 repository', () => { SANDBOX_PATH, async () => await prepareGeneratorForRepository(SANDBOX_PATH), ) - .withArguments('dbmem --model decoratordefined'); + .withArguments('--datasource dbmem --model decoratordefined'); const expectedFile = path.join( SANDBOX_PATH, REPOSITORY_APP_PATH, @@ -239,7 +263,7 @@ describe('lb4 repository', () => { SANDBOX_PATH, async () => await prepareGeneratorForRepository(SANDBOX_PATH), ) - .withArguments('dbkv --model Defaultmodel'); + .withArguments('--datasource dbkv --model Defaultmodel'); const expectedFile = path.join( SANDBOX_PATH, REPOSITORY_APP_PATH, @@ -302,7 +326,7 @@ const SANDBOX_FILES = [ path: CONFIG_PATH, file: 'myconfig.json', content: `{ - "name": "dbmem", + "datasource": "dbmem", "model": "decoratordefined" }`, }, From a984ef5d7a8539bac746788d9893f9b98769b212 Mon Sep 17 00:00:00 2001 From: Mario Estrada Date: Sat, 15 Sep 2018 01:01:43 -0600 Subject: [PATCH 4/6] feat(cli): add util shared functions and constants --- docs/site/Repository-generator.md | 8 +- packages/cli/generators/model/index.js | 7 +- packages/cli/generators/repository/index.js | 80 +++++++++++-------- .../repository-crud-default-template.ts.ejs | 3 +- .../repository-kv-template.ts.ejs | 4 +- packages/cli/lib/utils.js | 21 +++++ .../generators/repository.integration.js | 12 +-- 7 files changed, 83 insertions(+), 52 deletions(-) diff --git a/docs/site/Repository-generator.md b/docs/site/Repository-generator.md index ce835328defc..b2622ca3e0fb 100644 --- a/docs/site/Repository-generator.md +++ b/docs/site/Repository-generator.md @@ -45,8 +45,8 @@ file. ### Notes -Service oriented datasources such as REST or SOAP are not considered valid and -thus not presented to you in the selection list. +Service oriented datasources such as REST or SOAP are not considered valid in +this context and will not be presented to you in the selection list. There should be at least one valid _(KeyValue or Persisted)_ data source and one model already created in their respective directories. @@ -65,12 +65,12 @@ The tool will prompt you for: - **Please select the datasource.** _(name)_ If the name of the datasource had been supplied from the command line, the prompt is skipped, otherwise it will - present you, the list of available datasources to select one. It will use this + present you the list of available datasources to select one. It will use this datasource to check what kind of repository it will generate. - **Select the model(s) you want to generate a repository.** _(model)_ If the name of the model had been supplied from the command line with `--model` - option and it is a valid model, the the prompt is skipped, otherwise it will + option and it is a valid model, then the prompt is skipped, otherwise it will present the error `Error: No models found` in the console. If no `--model` is supplied, then the it will present you with a valid list of diff --git a/packages/cli/generators/model/index.js b/packages/cli/generators/model/index.js index 844a07deb729..8fff3babaa0c 100644 --- a/packages/cli/generators/model/index.js +++ b/packages/cli/generators/model/index.js @@ -30,12 +30,12 @@ module.exports = class ModelGenerator extends ArtifactGenerator { _setupGenerator() { this.artifactInfo = { type: 'model', - rootDir: 'src', + rootDir: utils.sourceRootDir, }; this.artifactInfo.outDir = path.resolve( this.artifactInfo.rootDir, - 'models', + utils.modelsDir, ); // Model Property Types @@ -190,8 +190,7 @@ module.exports = class ModelGenerator extends ArtifactGenerator { debug('scaffolding'); // Data for templates - this.artifactInfo.fileName = utils.kebabCase(this.artifactInfo.name); - this.artifactInfo.outFile = `${this.artifactInfo.fileName}.model.ts`; + this.artifactInfo.outFile = utils.getModelFileName(this.artifactInfo.name); // Resolved Output Path const tsPath = this.destinationPath( diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js index 49498d048bf2..2a5f6a1db5e8 100644 --- a/packages/cli/generators/repository/index.js +++ b/packages/cli/generators/repository/index.js @@ -47,7 +47,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { let fileContent = ''; let modelFile = path.join( this.artifactInfo.modelDir, - `${utils.kebabCase(modelName)}.model.ts`, + utils.getModelFileName(modelName), ); try { fileContent = this.fs.read(modelFile, {}); @@ -63,12 +63,12 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { * helper method to inspect and validate a repository type */ async _inferRepositoryType() { - if (!this.artifactInfo.dataSourceClassName) { + if (!this.artifactInfo.dataSourceClass) { return; } let result = this._isConnectorOfType( KEY_VALUE_CONNECTOR, - this.artifactInfo.dataSourceClassName, + this.artifactInfo.dataSourceClass, ); debug(`KeyValue Connector: ${result}`); @@ -81,17 +81,19 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { } // assign the data source name to the information artifact - let dataSourceName = this.artifactInfo.dataSourceClassName + let dataSourceName = this.artifactInfo.dataSourceClass .replace('Datasource', '') .toLowerCase(); - let dataSourceImportName = this.artifactInfo.dataSourceClassName.replace( + + let dataSourceClassName = this.artifactInfo.dataSourceClass.replace( 'Datasource', 'DataSource', ); Object.assign(this.artifactInfo, { - dataSourceImportName: dataSourceImportName, + dataSourceClassName: dataSourceClassName, }); + Object.assign(this.artifactInfo, { dataSourceName: dataSourceName, }); @@ -102,19 +104,17 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { * connectorType supplied for the given connector name * @param {string} connectorType single or a comma separated string array */ - _isConnectorOfType(connectorType, dataSourceClassName) { + _isConnectorOfType(connectorType, dataSourceClass) { debug(`calling isConnectorType ${connectorType}`); let jsonFileContent = ''; let result = false; - if (!dataSourceClassName) { + if (!dataSourceClass) { return false; } let datasourceJSONFile = path.join( this.artifactInfo.datasourcesDir, - dataSourceClassName - .replace('Datasource', '.datasource.json') - .toLowerCase(), + dataSourceClass.replace('Datasource', '.datasource.json').toLowerCase(), ); try { @@ -141,20 +141,20 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { _setupGenerator() { this.artifactInfo = { type: 'repository ', - rootDir: 'src', + rootDir: utils.sourceRootDir, }; this.artifactInfo.outDir = path.resolve( this.artifactInfo.rootDir, - 'repositories', + utils.repositoriesDir, ); this.artifactInfo.datasourcesDir = path.resolve( this.artifactInfo.rootDir, - 'datasources', + utils.datasourcesDir, ); this.artifactInfo.modelDir = path.resolve( this.artifactInfo.rootDir, - 'models', + utils.modelsDir, ); this.artifactInfo.defaultTemplate = REPOSITORY_CRUD_TEMPLATE; @@ -230,7 +230,11 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { 'datasource', true, ); - debug(`datasourcesList from src/datasources : ${datasourcesList}`); + debug( + `datasourcesList from ${utils.sourceRootDir}/${ + utils.datasourcesDir + } : ${datasourcesList}`, + ); } catch (err) { return this.exit(err); } @@ -244,17 +248,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { return result; }); - if (availableDatasources.includes(cmdDatasourceName)) { - Object.assign(this.artifactInfo, { - dataSourceClassName: cmdDatasourceName, - }); - } - - debug( - `artifactInfo.dataSourceClassName ${ - this.artifactInfo.dataSourceClassName - }`, - ); + debug(`artifactInfo.dataSourceClass ${this.artifactInfo.dataSourceClass}`); if (availableDatasources.length === 0) { return this.exit( @@ -267,13 +261,19 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { ); } + if (availableDatasources.includes(cmdDatasourceName)) { + Object.assign(this.artifactInfo, { + dataSourceClass: cmdDatasourceName, + }); + } + return this.prompt([ { type: 'list', - name: 'dataSourceClassName', + name: 'dataSourceClass', message: PROMPT_MESSAGE_DATA_SOURCE, choices: availableDatasources, - when: !this.artifactInfo.dataSourceClassName, + when: !this.artifactInfo.dataSourceClass, default: availableDatasources[0], validate: utils.validateClassName, }, @@ -310,7 +310,11 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { this.options.model = utils.toClassName(this.options.model); // assign the model name from the command line only if it is valid - if (modelList.length > 0 && modelList.includes(this.options.model)) { + if ( + modelList && + modelList.length > 0 && + modelList.includes(this.options.model) + ) { Object.assign(this.artifactInfo, {modelNameList: [this.options.model]}); } else { modelList = []; @@ -399,8 +403,9 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { if (this.options.name) { this.artifactInfo.className = utils.toClassName(this.options.name); - this.artifactInfo.outFile = - utils.kebabCase(this.options.name) + '.repository.ts'; + this.artifactInfo.outFile = utils.getRepositoryFileName( + this.options.name, + ); // make sure the name supplied from cmd line is only used once delete this.options.name; @@ -408,8 +413,9 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { this.artifactInfo.className = utils.toClassName( this.artifactInfo.modelName, ); - this.artifactInfo.outFile = - utils.kebabCase(this.artifactInfo.modelName) + '.repository.ts'; + this.artifactInfo.outFile = utils.getRepositoryFileName( + this.artifactInfo.modelName, + ); } if (debug.enabled) { @@ -417,7 +423,11 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { } const source = this.templatePath( - path.join('src', 'repositories', this.artifactInfo.defaultTemplate), + path.join( + utils.sourceRootDir, + utils.repositoriesDir, + this.artifactInfo.defaultTemplate, + ), ); if (debug.enabled) { diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs index 2ed7c98b06da..b982ce532583 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs @@ -1,5 +1,6 @@ import {<%= repositoryTypeClass %>, juggler} from '@loopback/repository'; import {<%= modelName %>} from '../models'; +import {<%= dataSourceClassName %>} from '../datasources'; import {inject} from '@loopback/core'; export class <%= className %>Repository extends <%= repositoryTypeClass %>< @@ -7,7 +8,7 @@ export class <%= className %>Repository extends <%= repositoryTypeClass %>< typeof <%= modelName %>.prototype.<%= idType %> > { constructor( - @inject('datasources.<%= dataSourceName %>') protected datasource: juggler.DataSource, + @inject('datasources.<%= dataSourceName %>') protected datasource: <%= dataSourceClassName %>, ) { super(<%= modelName %>, datasource); } diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs index 5d5ffc251b87..fb127b86e263 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs @@ -1,13 +1,13 @@ import {<%= repositoryTypeClass %>} from '@loopback/repository'; import {<%= modelName %>} from '../models'; -import {<%= dataSourceImportName %>} from '../datasources'; +import {<%= dataSourceClassName %>} from '../datasources'; import {inject} from '@loopback/core'; export class <%= className %>Repository extends <%= repositoryTypeClass %>< <%= modelName %> > { constructor( - @inject('datasources.<%= dataSourceName %>') datasource: <%= dataSourceImportName %>, + @inject('datasources.<%= dataSourceName %>') datasource: <%= dataSourceClassName %>, ) { super(<%= modelName %>,datasource); } diff --git a/packages/cli/lib/utils.js b/packages/cli/lib/utils.js index 7e0357bb79ae..2b34f2a14a7a 100644 --- a/packages/cli/lib/utils.js +++ b/packages/cli/lib/utils.js @@ -370,3 +370,24 @@ function validateValue(name, unallowedCharacters) { } return true; } +/** + * Returns the modelName in the directory file format for the model + * @param {string} modelName + */ +exports.getModelFileName = function(modelName) { + return `${_.kebabCase(modelName)}.model.ts`; +}; + +/** + * Returns the repositoryName in the directory file format for the repository + * @param {string} repositoryName + */ +exports.getRepositoryFileName = function(repositoryName) { + return `${_.kebabCase(repositoryName)}.repository.ts`; +}; + +// literal strings with artifacts directory locations +exports.repositoriesDir = 'repositories'; +exports.datasourcesDir = 'datasources'; +exports.modelsDir = 'models'; +exports.sourceRootDir = 'src'; diff --git a/packages/cli/test/integration/generators/repository.integration.js b/packages/cli/test/integration/generators/repository.integration.js index 5984f4ec2ecc..1af0d53a9e90 100644 --- a/packages/cli/test/integration/generators/repository.integration.js +++ b/packages/cli/test/integration/generators/repository.integration.js @@ -30,7 +30,7 @@ describe('lb4 repository', () => { describe('generate repositories on special conditions', () => { it('generates a multi-word crud repository', async () => { const multiItemPrompt = { - dataSourceClassName: 'DbmemDatasource', + dataSourceClass: 'DbmemDatasource', modelNameList: ['MultiWord'], }; @@ -116,7 +116,7 @@ describe('lb4 repository', () => { it('generates a repository asking for the ID name', async () => { const multiItemPrompt = { - dataSourceClassName: 'DbmemDatasource', + dataSourceClass: 'DbmemDatasource', modelNameList: ['InvalidId'], propertyName: 'myid', }; @@ -152,7 +152,7 @@ describe('lb4 repository', () => { describe('all invalid parameters and usage', () => { it('does not run with an invalid model name', async () => { const basicPrompt = { - dataSourceClassName: 'DbmemDatasource', + dataSourceClass: 'DbmemDatasource', }; return expect( testUtils @@ -168,7 +168,7 @@ describe('lb4 repository', () => { it("does not run when user doesn't select a model", async () => { const basicPrompt = { - dataSourceClassName: 'DbmemDatasource', + dataSourceClass: 'DbmemDatasource', }; return expect( testUtils @@ -197,7 +197,7 @@ describe('lb4 repository', () => { describe('valid generation of crud repositories', () => { it('generates a crud repository from default model', async () => { const basicPrompt = { - dataSourceClassName: 'DbmemDatasource', + dataSourceClass: 'DbmemDatasource', }; await testUtils .executeGenerator(generator) @@ -283,7 +283,7 @@ describe('lb4 repository', () => { it('generates a kv repository from decorator defined model', async () => { const basicPrompt = { - dataSourceClassName: 'DbkvDatasource', + dataSourceClass: 'DbkvDatasource', }; await testUtils .executeGenerator(generator) From e2de6ce322fdcbc9007764c97ffb9755c44fed21 Mon Sep 17 00:00:00 2001 From: Mario Estrada Date: Tue, 18 Sep 2018 01:01:24 -0600 Subject: [PATCH 5/6] feat(cli): add test for multiple repositories --- docs/site/Repository-generator.md | 5 +- docs/site/tables/lb4-artifact-commands.html | 2 +- packages/cli/generators/repository/index.js | 28 ++++----- .../repository-crud-default-template.ts.ejs | 2 +- .../repository-kv-template.ts.ejs | 2 +- packages/cli/lib/artifact-generator.js | 10 ++-- .../generators/repository.integration.js | 57 +++++++++++++++++++ 7 files changed, 84 insertions(+), 22 deletions(-) diff --git a/docs/site/Repository-generator.md b/docs/site/Repository-generator.md index b2622ca3e0fb..eb9c0efb9e61 100644 --- a/docs/site/Repository-generator.md +++ b/docs/site/Repository-generator.md @@ -10,8 +10,9 @@ permalink: /doc/en/lb4/Repository-generator.html ### Synopsis -Adds a new [Repository or Multiple Repositories](Repositories.md) class to a -LoopBack application with one single command. +Adds a new +[Repository class (or multiple backed by the same datasource)](Repositories.md) +to a LoopBack application with one single command. ```sh lb4 repository [options] [] diff --git a/docs/site/tables/lb4-artifact-commands.html b/docs/site/tables/lb4-artifact-commands.html index 2848d5eedd2d..ee56b14b6492 100644 --- a/docs/site/tables/lb4-artifact-commands.html +++ b/docs/site/tables/lb4-artifact-commands.html @@ -31,7 +31,7 @@ lb4 repository - Add a new repository or multiple repositories to a LoopBack 4 application + Add new repositories for selected model(s) to a LoopBack 4 application Repository generator diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js index 2a5f6a1db5e8..e9b27ee409c2 100644 --- a/packages/cli/generators/repository/index.js +++ b/packages/cli/generators/repository/index.js @@ -43,7 +43,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { * get the property name for the id field * @param {string} modelName */ - async _getModelIdType(modelName) { + async _getModelIdProperty(modelName) { let fileContent = ''; let modelFile = path.join( this.artifactInfo.modelDir, @@ -157,6 +157,9 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { 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', { @@ -354,7 +357,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { async promptModelId() { if (this.shouldExit()) return false; - let idType; + let idProperty; debug(`Model ID property name from command line: ${this.options.id}`); debug(`Selected Models: ${this.artifactInfo.modelNameList}`); @@ -379,19 +382,19 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { if (this.options.id) { debug(`passing thru this.options.id with value : ${this.options.id}`); - idType = 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 { - idType = await this._getModelIdType(item); - if (idType === null) { + idProperty = await this._getModelIdProperty(item); + if (idProperty === null) { const answer = await this.prompt(prompts); - idType = answer.propertyName; + idProperty = answer.propertyName; } } - this.artifactInfo.idType = idType; + this.artifactInfo.idProperty = idProperty; // Generate this repository await this._scaffold(); } @@ -413,13 +416,15 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { this.artifactInfo.className = utils.toClassName( this.artifactInfo.modelName, ); + this.artifactInfo.outFile = utils.getRepositoryFileName( this.artifactInfo.modelName, ); - } - if (debug.enabled) { - debug(`Artifact output filename set to: ${this.artifactInfo.outFile}`); + this.artifactInfo.indexesToBeUpdated.push({ + dir: this.artifactInfo.outDir, + file: this.artifactInfo.outFile, + }); } const source = this.templatePath( @@ -430,9 +435,6 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { ), ); - if (debug.enabled) { - debug(`Using template at: ${source}`); - } const dest = this.destinationPath( path.join(this.artifactInfo.outDir, this.artifactInfo.outFile), ); diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs index b982ce532583..d08a59e0579b 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-crud-default-template.ts.ejs @@ -5,7 +5,7 @@ import {inject} from '@loopback/core'; export class <%= className %>Repository extends <%= repositoryTypeClass %>< <%= modelName %>, - typeof <%= modelName %>.prototype.<%= idType %> + typeof <%= modelName %>.prototype.<%= idProperty %> > { constructor( @inject('datasources.<%= dataSourceName %>') protected datasource: <%= dataSourceClassName %>, diff --git a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs index fb127b86e263..4fad4863a068 100644 --- a/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs +++ b/packages/cli/generators/repository/templates/src/repositories/repository-kv-template.ts.ejs @@ -9,6 +9,6 @@ export class <%= className %>Repository extends <%= repositoryTypeClass %>< constructor( @inject('datasources.<%= dataSourceName %>') datasource: <%= dataSourceClassName %>, ) { - super(<%= modelName %>,datasource); + super(<%= modelName %>, datasource); } } diff --git a/packages/cli/lib/artifact-generator.js b/packages/cli/lib/artifact-generator.js index 3e5cd0b0f687..f843660f73be 100644 --- a/packages/cli/lib/artifact-generator.js +++ b/packages/cli/lib/artifact-generator.js @@ -129,17 +129,19 @@ module.exports = class ArtifactGenerator extends BaseGenerator { // Index Update Disabled if (this.artifactInfo.disableIndexUpdate) return; + if (!this.artifactInfo.indexesToBeUpdated) { + this.artifactInfo.indexesToBeUpdated = []; + } + // No Array given for Index Update, Create default array if ( - !this.artifactInfo.indexesToBeUpdated && this.artifactInfo.outDir && - this.artifactInfo.outFile + this.artifactInfo.outFile && + this.artifactInfo.indexesToBeUpdated.length === 0 ) { this.artifactInfo.indexesToBeUpdated = [ {dir: this.artifactInfo.outDir, file: this.artifactInfo.outFile}, ]; - } else { - this.artifactInfo.indexesToBeUpdated = []; } for (const idx of this.artifactInfo.indexesToBeUpdated) { diff --git a/packages/cli/test/integration/generators/repository.integration.js b/packages/cli/test/integration/generators/repository.integration.js index 1af0d53a9e90..f8d1bf5c6a6e 100644 --- a/packages/cli/test/integration/generators/repository.integration.js +++ b/packages/cli/test/integration/generators/repository.integration.js @@ -28,6 +28,63 @@ describe('lb4 repository', () => { // special cases regardless of the repository type describe('generate repositories on special conditions', () => { + it('generates multipe crud repositories', async () => { + const multiItemPrompt = { + dataSourceClass: 'DbmemDatasource', + modelNameList: ['MultiWord', 'Defaultmodel'], + }; + + await testUtils + .executeGenerator(generator) + .inDir( + SANDBOX_PATH, + async () => await prepareGeneratorForRepository(SANDBOX_PATH), + ) + .withPrompts(multiItemPrompt); + + const expectedMultiWordFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'multi-word.repository.ts', + ); + const expectedDefaultModelFile = path.join( + SANDBOX_PATH, + REPOSITORY_APP_PATH, + 'defaultmodel.repository.ts', + ); + + assert.file(expectedMultiWordFile); + assert.file(expectedDefaultModelFile); + + assert.fileContent( + expectedMultiWordFile, + /export class MultiWordRepository extends DefaultCrudRepository { const multiItemPrompt = { dataSourceClass: 'DbmemDatasource', From 88dbb8703b15a2683483901b99f8ee2f972715e3 Mon Sep 17 00:00:00 2001 From: Mario Estrada Date: Tue, 18 Sep 2018 09:39:53 -0600 Subject: [PATCH 6/6] feat(cli): change msg when no datasource or repository exists --- packages/cli/generators/repository/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/generators/repository/index.js b/packages/cli/generators/repository/index.js index e9b27ee409c2..39fcd95ee623 100644 --- a/packages/cli/generators/repository/index.js +++ b/packages/cli/generators/repository/index.js @@ -28,8 +28,8 @@ const PROMPT_MESSAGE_MODEL = 'Select the model(s) you want to generate a repository'; const PROMPT_MESSAGE_DATA_SOURCE = 'Please select the datasource'; const ERROR_READING_FILE = 'Error reading file'; -const ERROR_NO_DATA_SOURCES_FOUND = 'No datasources found in'; -const ERROR_NO_MODELS_FOUND = 'No models found in'; +const ERROR_NO_DATA_SOURCES_FOUND = 'No datasources found at'; +const ERROR_NO_MODELS_FOUND = 'No models found at'; const ERROR_NO_MODEL_SELECTED = 'You did not select a valid model'; const ERROR_NO_DIRECTORY = 'The directory was not found'; @@ -192,7 +192,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { if (!fs.existsSync(this.artifactInfo.datasourcesDir)) { return this.exit( new Error( - `${ERROR_NO_DIRECTORY} ${this.artifactInfo.datasourcesDir}. + `${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', )}`, @@ -204,7 +204,7 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator { if (!fs.existsSync(this.artifactInfo.modelDir)) { return this.exit( new Error( - `${ERROR_NO_DIRECTORY} ${this.artifactInfo.modelDir}. + `${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', )}`,