From 7714bbbacd8a6832b873c9a7f7524f611e25545c Mon Sep 17 00:00:00 2001 From: frodo Date: Thu, 10 Jan 2019 13:29:59 -0800 Subject: [PATCH] feat(cli): add lb4 discover for model discovery --- docs/site/Discovering-models.md | 45 ++++ docs/site/Model.md | 6 + docs/site/sidebars/lb4_sidebar.yml | 4 + docs/site/tables/lb4-artifact-commands.html | 7 + packages/cli/README.md | 30 ++- packages/cli/generators/discover/index.js | 255 ++++++++++++++++++ packages/cli/generators/model/index.js | 86 ++++++ packages/cli/lib/artifact-generator.js | 12 +- packages/cli/lib/cli.js | 4 + packages/cli/lib/model-discoverer.js | 108 ++++++++ packages/cli/lib/update-index.js | 2 + packages/cli/package-lock.json | 186 +++++++++++++ packages/cli/package.json | 3 +- packages/cli/test/fixtures/discover/index.js | 9 + .../fixtures/discover/mem.datasource.js.txt | 105 ++++++++ .../test/integration/cli/cli.integration.js | 4 +- .../generators/discover.integration.js | 127 +++++++++ .../generators/model.integration.js | 54 ++-- .../cli/test/integration/lib/file-check.js | 27 ++ packages/cli/test/test-utils.js | 4 +- 20 files changed, 1042 insertions(+), 36 deletions(-) create mode 100644 docs/site/Discovering-models.md create mode 100644 packages/cli/generators/discover/index.js create mode 100644 packages/cli/lib/model-discoverer.js create mode 100644 packages/cli/test/fixtures/discover/index.js create mode 100644 packages/cli/test/fixtures/discover/mem.datasource.js.txt create mode 100644 packages/cli/test/integration/generators/discover.integration.js create mode 100644 packages/cli/test/integration/lib/file-check.js diff --git a/docs/site/Discovering-models.md b/docs/site/Discovering-models.md new file mode 100644 index 000000000000..222b3142cdba --- /dev/null +++ b/docs/site/Discovering-models.md @@ -0,0 +1,45 @@ +--- +lang: en +title: 'Discovering models from relational databases' +keywords: LoopBack 4.0, LoopBack-Next +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Discovering-models.html +--- + +## Synopsis + +LoopBack makes it simple to create models from an existing relational database. +This process is called _discovery_ and is supported by the following connectors: + +- Cassandra +- MySQL +- Oracle +- PostgreSQL +- SQL Server +- IBM DB2 +- IBM DashDB +- IBM DB2 for z/OS +- [SAP HANA](https://www.npmjs.org/package/loopback-connector-saphana) - Not + officially supported; + +## Overview + +Models can be discovered from a supported datasource by running the +`lb4 discover` command. + +**The LoopBack project must be built and contain the built datasource files in +`PROJECT_DIR/dist/datasources/*.js`** + +### Options + +`--dataSource`: Put a valid datasource name here to skip the datasource prompt + +`--views`: Choose whether to discover views. Default is true + +`--all`: Skips the model prompt and discovers all of them + +`--outDir`: Specify the directory into which the `model.model.ts` files will be +placed. Default is `src/models` + +`--schema`: Specify the schema which the datasource will find the models to +discover diff --git a/docs/site/Model.md b/docs/site/Model.md index 94df9fa4d095..e542918a4440 100644 --- a/docs/site/Model.md +++ b/docs/site/Model.md @@ -97,6 +97,12 @@ export class Customer { } ``` +## Model Discovery + +LoopBack can automatically create model definitions by discovering the schema of +your database. See [Discovering models](Discovering-models.md) for more details +and a list of connectors supporting model discovery. + ## Using the Juggler Bridge To define a model for use with the juggler bridge, extend your classes from diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index b6ba1c9a0076..1cef0ace6e8f 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -287,6 +287,10 @@ children: url: Model-generator.html output: 'web, pdf' + - title: 'Model discovery' + url: Discovering-models.html + output: 'web, pdf' + - title: 'Repository generator' url: Repository-generator.html output: 'web, pdf' diff --git a/docs/site/tables/lb4-artifact-commands.html b/docs/site/tables/lb4-artifact-commands.html index e9646f84bbdc..1ad18ab32557 100644 --- a/docs/site/tables/lb4-artifact-commands.html +++ b/docs/site/tables/lb4-artifact-commands.html @@ -47,11 +47,18 @@ OpenAPI generator + + lb4 discover + Discover models from relational databases + Model Discovery + + lb4 observer Generate life cycle observers for application start/stop Life cycle observer generator + diff --git a/packages/cli/README.md b/packages/cli/README.md index 80daa42942f8..75fd4cdb8bc2 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -275,9 +275,35 @@ Run the following command to install the CLI. Arguments: name # Name for the observer Type: String Required: false + + ``` + +11. To discover a model from a supported datasource + + ```sh + cd + lb4 discover + lb4 discover [] [options] + + Options: + -h, --help # Print the generator's options and usage + --skip-cache # Do not remember prompt answers Default: false + --skip-install # Do not automatically install dependencies Default: false + --force-install # Fail on install dependencies error Default: false + -c, --config # JSON file name or value to configure options + -y, --yes # Skip all confirmation prompts with default or provided value + --format # Format generated code using npm run lint:fix + -ds, --dataSource # The name of the datasource to discover + --views # Boolean to discover views Default: true + --schema # Schema to discover + --all # Discover all models without prompting users to select Default: false + --outDir # Specify the directory into which the `model.model.ts` files will be placed + + Arguments: + name # Name for the discover Type: String Required: false ``` -11. To list available commands +12. To list available commands `lb4 --commands` (or `lb4 -l`) @@ -296,7 +322,7 @@ Run the following command to install the CLI. Please note `lb4 --help` also prints out available commands. -12. To print out version information +13. To print out version information `lb4 --version` (or `lb4 -v`) diff --git a/packages/cli/generators/discover/index.js b/packages/cli/generators/discover/index.js new file mode 100644 index 000000000000..3d939df6c5aa --- /dev/null +++ b/packages/cli/generators/discover/index.js @@ -0,0 +1,255 @@ +path = require('path'); +const fs = require('fs'); +const ArtifactGenerator = require('../../lib/artifact-generator'); +const modelMaker = require('../../lib/model-discoverer'); +const debug = require('../../lib/debug')('discover-generator'); +const utils = require('../../lib/utils'); +const modelDiscoverer = require('../../lib/model-discoverer'); +const rootDir = 'src'; + +module.exports = class DiscoveryGenerator extends ArtifactGenerator { + constructor(args, opts) { + super(args, opts); + + this.option('dataSource', { + type: String, + alias: 'ds', + description: 'The name of the datasource to discover', + }); + + this.option('views', { + type: Boolean, + description: 'Boolean to discover views', + default: true, + }); + + this.option('schema', { + type: String, + description: 'Schema to discover', + default: '', + }); + + this.option('all', { + type: Boolean, + description: 'Discover all models without prompting users to select', + default: false, + }); + + this.option('outDir', { + type: String, + description: + 'Specify the directory into which the `model.model.ts` files will be placed', + default: undefined, + }); + } + + _setupGenerator() { + this.artifactInfo = { + type: 'discover', + rootDir, + outDir: path.resolve(rootDir, 'models'), + }; + + return super._setupGenerator(); + } + + /** + * If we have a dataSource, attempt to load it + * @returns {*} + */ + setOptions() { + if (this.options.dataSource) { + debug(`Data source specified: ${this.options.dataSource}`); + this.artifactInfo.dataSource = modelMaker.loadDataSourceByName( + this.options.dataSource, + ); + } + + return super.setOptions(); + } + + /** + * Ensure CLI is being run in a LoopBack 4 project. + */ + checkLoopBackProject() { + if (this.shouldExit()) return; + return super.checkLoopBackProject(); + } + + /** + * Loads all datasources to choose if the dataSource option isn't set + */ + async loadAllDatasources() { + // If we have a dataSourcePath then it is already loaded for us, we don't need load any + if (this.artifactInfo.dataSource) { + return; + } + const dsDir = modelMaker.DEFAULT_DATASOURCE_DIRECTORY; + const datasourcesList = await utils.getArtifactList( + dsDir, + 'datasource', + false, + ); + debug(datasourcesList); + + this.dataSourceChoices = datasourcesList.map(s => + modelDiscoverer.loadDataSource( + path.resolve(dsDir, `${utils.kebabCase(s)}.datasource.js`), + ), + ); + debug(`Done importing datasources`); + } + + /** + * Ask the user to select the data source from which to discover + */ + promptDataSource() { + if (this.shouldExit()) return; + const prompts = [ + { + name: 'dataSource', + message: `Select the connector to discover`, + type: 'list', + choices: this.dataSourceChoices, + when: + this.artifactInfo.dataSource === undefined && + !this.artifactInfo.modelDefinitions, + }, + ]; + + return this.prompt(prompts).then(answer => { + if (!answer.dataSource) return; + debug(`Datasource answer: ${JSON.stringify(answer)}`); + + this.artifactInfo.dataSource = this.dataSourceChoices.find( + d => d.name === answer.dataSource, + ); + }); + } + + /** + * Puts all discoverable models in this.modelChoices + */ + async discoverModelInfos() { + if (this.artifactInfo.modelDefinitions) return; + debug(`Getting all models from ${this.artifactInfo.dataSource.name}`); + + this.modelChoices = await modelMaker.discoverModelNames( + this.artifactInfo.dataSource, + {views: this.options.views, schema: this.options.schema}, + ); + debug( + `Got ${this.modelChoices.length} models from ${ + this.artifactInfo.dataSource.name + }`, + ); + } + + /** + * Now that we have a list of all models for a datasource, + * ask which models to discover + */ + promptModelChoices() { + // If we are discovering all we don't need to prompt + if (this.options.all) { + this.discoveringModels = this.modelChoices; + } + + const prompts = [ + { + name: 'discoveringModels', + message: `Select the models which to discover`, + type: 'checkbox', + choices: this.modelChoices, + when: + this.discoveringModels === undefined && + !this.artifactInfo.modelDefinitions, + }, + ]; + + return this.prompt(prompts).then(answers => { + if (!answers.discoveringModels) return; + debug(`Models chosen: ${JSON.stringify(answers)}`); + this.discoveringModels = []; + answers.discoveringModels.forEach(m => { + this.discoveringModels.push(this.modelChoices.find(c => c.name === m)); + }); + }); + } + + /** + * Using artifactInfo.dataSource, + * artifactInfo.modelNameOptions + * + * this will discover every model + * and put it in artifactInfo.modelDefinitions + * @return {Promise} + */ + async getAllModelDefs() { + this.artifactInfo.modelDefinitions = []; + for (let i = 0; i < this.discoveringModels.length; i++) { + const modelInfo = this.discoveringModels[i]; + debug(`Discovering: ${modelInfo.name}...`); + this.artifactInfo.modelDefinitions.push( + await modelMaker.discoverSingleModel( + this.artifactInfo.dataSource, + modelInfo.name, + {schema: modelInfo.schema}, + ), + ); + debug(`Discovered: ${modelInfo.name}`); + } + } + + /** + * Iterate through all the models we have discovered and scaffold + */ + async scaffold() { + this.artifactInfo.indexesToBeUpdated = + this.artifactInfo.indexesToBeUpdated || []; + + // Exit if needed + if (this.shouldExit()) return false; + + for (let i = 0; i < this.artifactInfo.modelDefinitions.length; i++) { + const modelDefinition = this.artifactInfo.modelDefinitions[i]; + Object.entries(modelDefinition.properties).forEach(([k, v]) => + modelDiscoverer.sanitizeProperty(v), + ); + modelDefinition.isModelBaseBuiltin = true; + modelDefinition.modelBaseClass = 'Entity'; + modelDefinition.className = utils.pascalCase(modelDefinition.name); + // These last two are so that the templat doesn't error out of they aren't there + modelDefinition.allowAdditionalProperties = true; + modelDefinition.modelSettings = modelDefinition.settings || {}; + debug(`Generating: ${modelDefinition.name}`); + + const fullPath = path.resolve( + this.options.outDir || this.artifactInfo.outDir, + utils.getModelFileName(modelDefinition.name), + ); + debug(`Writing: ${fullPath}`); + + this.copyTemplatedFiles( + modelDiscoverer.MODEL_TEMPLATE_PATH, + fullPath, + modelDefinition, + ); + + this.artifactInfo.indexesToBeUpdated.push({ + dir: this.options.outDir || this.artifactInfo.outDir, + file: utils.getModelFileName(modelDefinition.name), + }); + } + + // This part at the end is just for the ArtifactGenerator + // end message to output something nice, before it was "Discover undefined was created in src/models/" + this.artifactInfo.name = this.artifactInfo.modelDefinitions + .map(d => utils.getModelFileName(d.name)) + .join(','); + } + + async end() { + await super.end(); + } +}; diff --git a/packages/cli/generators/model/index.js b/packages/cli/generators/model/index.js index b2de7ad92840..7a56de1fd5f6 100644 --- a/packages/cli/generators/model/index.js +++ b/packages/cli/generators/model/index.js @@ -5,6 +5,9 @@ 'use strict'; +const modelDiscoverer = require('../../lib/model-discoverer'); +const fs = require('fs'); + const ArtifactGenerator = require('../../lib/artifact-generator'); const debug = require('../../lib/debug')('model-generator'); const inspect = require('util').inspect; @@ -14,6 +17,7 @@ const path = require('path'); const PROMPT_BASE_MODEL_CLASS = 'Please select the model base class'; const ERROR_NO_MODELS_FOUND = 'Model was not found in'; + const BASE_MODELS = ['Entity', 'Model']; const CLI_BASE_MODELS = [ { @@ -87,6 +91,27 @@ module.exports = class ModelGenerator extends ArtifactGenerator { // This flag is to indicate whether the base class has been validated. this.isBaseClassChecked = false; + this.option('dataSource', { + type: String, + required: false, + description: + 'The name of the dataSource which contains this model and suppots model discovery', + }); + + this.option('table', { + type: String, + required: false, + description: + 'If discovering a model from a dataSource, specify the name of its table/view', + }); + + this.option('schema', { + type: String, + required: false, + description: + 'If discovering a model from a dataSource, specify the schema which contains it', + }); + return super._setupGenerator(); } @@ -99,6 +124,63 @@ module.exports = class ModelGenerator extends ArtifactGenerator { return super.checkLoopBackProject(); } + async getDataSource() { + if (!this.options.dataSource) { + debug('Not loading any dataSources because none specified'); + return; + } + + this.artifactInfo.dataSource = modelDiscoverer.loadDataSourceByName( + this.options.dataSource, + ); + + if (!this.artifactInfo.dataSource) { + const s = `Could not find dataSource ${this.options.dataSource}`; + debug(s); + return this.exit( + new Error( + `${s}.${chalk.yellow( + 'Please visit https://loopback.io/doc/en/lb4/Model-generator.html for information on how models are discovered', + )}`, + ), + ); + } + } + + // Use the dataSource to discover model properties + async discoverModelPropertiesWithDatasource() { + if (this.shouldExit()) return false; + if (!this.options.dataSource) return; + if (!this.artifactInfo.dataSource) { + } + + const schemaDef = await modelDiscoverer.discoverSingleModel( + this.artifactInfo.dataSource, + this.options.table, + { + schema: this.options.schema, + views: true, + }, + ); + + if (!schemaDef) { + this.exit( + new Error( + `Could not locate table: ${this.options.table} in schema: ${ + this.options.schema + } + ${chalk.yellow( + 'Please visit https://loopback.io/doc/en/lb4/Model-generator.html for information on how models are discovered', + )}`, + ), + ); + } + + Object.assign(this.artifactInfo, schemaDef); + this.artifactInfo.defaultName = this.artifactInfo.name; + delete this.artifactInfo.name; + } + // Prompt a user for Model Name async promptArtifactName() { if (this.shouldExit()) return; @@ -363,6 +445,10 @@ module.exports = class ModelGenerator extends ArtifactGenerator { debug('scaffolding'); + Object.entries(this.artifactInfo.properties).forEach(([k, v]) => + modelDiscoverer.sanitizeProperty(v), + ); + // Data for templates this.artifactInfo.outFile = utils.getModelFileName(this.artifactInfo.name); diff --git a/packages/cli/lib/artifact-generator.js b/packages/cli/lib/artifact-generator.js index 8e7af1b839ee..ca8dbb65b820 100644 --- a/packages/cli/lib/artifact-generator.js +++ b/packages/cli/lib/artifact-generator.js @@ -52,7 +52,7 @@ module.exports = class ArtifactGenerator extends BaseGenerator { // capitalization message: utils.toClassName(this.artifactInfo.type) + ' class name:', when: this.artifactInfo.name === undefined, - default: this.artifactInfo.name, + default: this.artifactInfo.defaultName, validate: utils.validateClassName, }, ]; @@ -84,11 +84,14 @@ module.exports = class ArtifactGenerator extends BaseGenerator { return; } - let generationStatus = true; // Check all files being generated to ensure they succeeded - Object.entries(this.conflicter.generationStatus).forEach(([key, val]) => { - if (val === 'skip' || val === 'identical') generationStatus = false; + let generationStatus = !!Object.entries( + this.conflicter.generationStatus, + ).find(([key, val]) => { + // If a file was modified, update the indexes and say stuff about it + return val !== 'skip' && val !== 'identical'; }); + debug(`Generation status: ${generationStatus}`); if (generationStatus) { await this._updateIndexFiles(); @@ -124,6 +127,7 @@ module.exports = class ArtifactGenerator extends BaseGenerator { * }, {dir: '...', file: '...'}] */ async _updateIndexFiles() { + debug(`Indexes to be updated ${this.artifactInfo.indexesToBeUpdated}`); // Index Update Disabled if (this.artifactInfo.disableIndexUpdate) return; diff --git a/packages/cli/lib/cli.js b/packages/cli/lib/cli.js index 0be7f52ba55c..b1acd5e7166e 100644 --- a/packages/cli/lib/cli.js +++ b/packages/cli/lib/cli.js @@ -83,6 +83,10 @@ function setupGenerators() { path.join(__dirname, '../generators/observer'), PREFIX + 'observer', ); + env.register( + path.join(__dirname, '../generators/discover'), + PREFIX + 'discover', + ); return env; } diff --git a/packages/cli/lib/model-discoverer.js b/packages/cli/lib/model-discoverer.js new file mode 100644 index 000000000000..b7c71921e944 --- /dev/null +++ b/packages/cli/lib/model-discoverer.js @@ -0,0 +1,108 @@ +const debug = require('./debug')('model-discoverer'); +const fs = require('fs'); +const path = require('path'); + +/** + * Given a datasource and discovery options, + * return a list of objects {table: 'foo', schema: 'bar} + */ +async function discoverModelNames(ds, options) { + if (!ds.connected) { + await new Promise(resolve => { + ds.on('connected', resolve); + }); + } + return await ds.discoverModelDefinitions(options); +} + +/** + * Returns the schema definition for a model + * @param ds {Juggler.DataSource} + * @param modelName {string} + * @param options {object} + * @return {Promise} + */ +async function discoverSingleModel(ds, modelName, options) { + const schema = await ds.discoverSchema(modelName, options); + if (schema) { + schema.settings = schema && schema.options; + } + return schema; +} + +/** + * Loads a DataSource from a file + * If the path provided is a JSON, it instantiates a juggler.DataSource with the config as the only argument + * Else it requires it like a compiled loopback datasource + * @param path + * @returns juggler.DataSource + */ +function loadDataSource(path) { + const ds = require(path); + const key = Object.keys(ds)[0]; + const val = new ds[key](); + return val; +} + +/** + * Loads a compiled loopback datasource by name + * @param name {string} + * @returns {*} + */ +function loadDataSourceByName(name) { + debug(`Searching for specified dataSource ${name}`); + const dataSourceFiles = getAllDataSourceFiles(); + debug(`Loaded ${dataSourceFiles.length} dataSource files`); + + for (let i = 0; i < dataSourceFiles.length; i++) { + const f = dataSourceFiles[i]; + const ds = loadDataSource(path.resolve(DEFAULT_DATASOURCE_DIRECTORY, f)); + if (ds.name === name) { + debug(`Found dataSource ${name}`); + return ds; + } else { + debug(`Did not match dataSource ${name} !== ${ds.name}`); + } + } + throw new Error( + `Cannot find datasource "${name}" in ${DEFAULT_DATASOURCE_DIRECTORY}`, + ); +} + +const DEFAULT_DATASOURCE_DIRECTORY = './dist/datasources'; + +const MODEL_TEMPLATE_PATH = path.resolve( + __dirname, + '../generators/model/templates/model.ts.ejs', +); + +const sanitizeProperty = function(o) { + Object.entries(o).forEach(([k, v]) => { + // Delete the null properties so the template doesn't spit out `key: ;` + if (v === null) { + delete o[k]; + } + + // If you are an object or array, stringify so you don't appear as [object [object] + if (v === Object(v)) { + o[k] = JSON.stringify(o[k]); + } + }); + + o.tsType = o.type; +}; + +function getAllDataSourceFiles(dir = DEFAULT_DATASOURCE_DIRECTORY) { + return fs.readdirSync(dir).filter(s => s.endsWith('.datasource.js')); +} + +module.exports = { + getAllDataSourceFiles, + sanitizeProperty, + discoverModelNames, + discoverSingleModel, + loadDataSource, + loadDataSourceByName, + DEFAULT_DATASOURCE_DIRECTORY, + MODEL_TEMPLATE_PATH, +}; diff --git a/packages/cli/lib/update-index.js b/packages/cli/lib/update-index.js index d4ecd0c79c88..71f563497aa4 100644 --- a/packages/cli/lib/update-index.js +++ b/packages/cli/lib/update-index.js @@ -2,6 +2,7 @@ // Node module: @loopback/cli // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +const debug = require('./debug')('update-index'); const path = require('path'); const util = require('util'); @@ -16,6 +17,7 @@ const exists = util.promisify(fs.exists); * @param {*} file The new file to be exported from index.ts */ module.exports = async function(dir, file) { + debug(`Updating index ${path.join(dir, file)}`); const indexFile = path.join(dir, 'index.ts'); if (!file.endsWith('.ts')) { throw new Error(`${file} must be a TypeScript (.ts) file`); diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 07aec1f21173..6b46257da91c 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -109,6 +109,16 @@ "through": ">=2.2.7 <3" } }, + "accept-language": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", + "integrity": "sha1-9QJfF79lpGaoRYOMz5jNuHfYM4Q=", + "dev": true, + "requires": { + "bcp47": "^1.1.2", + "stable": "^0.1.6" + } + }, "agent-base": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", @@ -330,6 +340,12 @@ } } }, + "bcp47": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", + "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=", + "dev": true + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -358,6 +374,16 @@ "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.1.2.tgz", "integrity": "sha512-xVNN69YGDghOqCCtA6FI7avYrr02mTJjOgB0/f1VPD3pJC8QEvjTKWc4epDx8AqxxA75NI0QpVM2gPJXUbE4Tg==" }, + "bl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "bluebird": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", @@ -566,6 +592,12 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, "chownr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", @@ -597,6 +629,12 @@ } } }, + "cldrjs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.1.tgz", + "integrity": "sha512-xyiP8uAm8K1IhmpDndZLraloW1yqu0L+HYdQ7O1aGPxx9Cr+BMnPANlNhSt++UKfxytL2hd2NPXgTjiy7k43Ew==", + "dev": true + }, "cli-boxes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", @@ -818,6 +856,12 @@ } } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -921,6 +965,12 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, "detect-conflict": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/detect-conflict/-/detect-conflict-1.0.1.tgz", @@ -1531,6 +1581,15 @@ "ini": "^1.3.4" } }, + "globalize": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.4.2.tgz", + "integrity": "sha512-IfKeYI5mAITBmT5EnH8kSQB5uGson4Fkj2XtTpyEbIS7IHNfLHoeTyLJ6tfjiKC6cJXng3IhVurDk5C7ORqFhQ==", + "dev": true, + "requires": { + "cldrjs": "^0.5.0" + } + }, "globby": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", @@ -1799,6 +1858,12 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2323,6 +2388,51 @@ "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", "dev": true }, + "loopback-connector": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-4.6.1.tgz", + "integrity": "sha512-2bVA3sMokZBoijxYhLJshNK5ADgdo4XA/j5sIItKyDvXvlkBvkpwP55G9qXw98PP3K5DC8tE+x597lFGY1MmFg==", + "dev": true, + "requires": { + "async": "^2.1.5", + "bluebird": "^3.4.6", + "debug": "^3.1.0", + "msgpack5": "^4.2.0", + "strong-globalize": "^4.1.1", + "uuid": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "loopback-datasource-juggler": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-4.6.0.tgz", + "integrity": "sha512-4NoP1/AzqofwB0waCXIFD77vZ5Dl7HWbGBCm3UZSnuRMp0k8r0o8pEfgJ6v/GblmLG1jNXM7r9xQRWnh8Jo3CA==", + "dev": true, + "requires": { + "async": "^2.6.0", + "debug": "^4.1.0", + "depd": "^2.0.0", + "inflection": "^1.6.0", + "lodash": "^4.17.4", + "loopback-connector": "^4.4.0", + "minimatch": "^3.0.3", + "qs": "^6.5.0", + "shortid": "^2.2.6", + "strong-globalize": "^4.1.1", + "traverse": "^0.6.6", + "uuid": "^3.0.1" + } + }, "lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", @@ -2417,6 +2527,17 @@ "object-visit": "^1.0.0" } }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "dev": true, + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, "mem": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", @@ -2637,6 +2758,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "msgpack5": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.2.1.tgz", + "integrity": "sha512-Xo7nE9ZfBVonQi1rSopNAqPdts/QHyuSEUwIEzAkB+V2FtmkkLUbP6MyVqVVQxsZYI65FpvW3Bb8Z9ZWEjbgHQ==", + "dev": true, + "requires": { + "bl": "^2.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.3.6", + "safe-buffer": "^5.1.2" + } + }, "multimatch": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", @@ -2653,6 +2786,12 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, + "nanoid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.1.tgz", + "integrity": "sha512-k1u2uemjIGsn25zmujKnotgniC/gxQ9sdegdezeDiKdkDW56THUMqlz3urndKCXJxA6yPzSZbXx/QCMe/pxqsA==", + "dev": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -3686,6 +3825,15 @@ "rechoir": "^0.6.2" } }, + "shortid": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz", + "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==", + "dev": true, + "requires": { + "nanoid": "^2.0.0" + } + }, "should": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", @@ -4001,6 +4149,12 @@ "figgy-pudding": "^3.5.1" } }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -4104,6 +4258,22 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "strong-globalize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-4.1.2.tgz", + "integrity": "sha512-2ks3/fuQy4B/AQDTAaEvTXYSqH4TWrv9VGlbZ4YujzijEJbIWbptF/9dO13duv87aRhWdM5ABEiTy7ZmnmBhdQ==", + "dev": true, + "requires": { + "accept-language": "^3.0.18", + "debug": "^4.0.1", + "globalize": "^1.3.0", + "lodash": "^4.17.4", + "md5": "^2.2.1", + "mkdirp": "^0.5.1", + "os-locale": "^3.0.1", + "yamljs": "^0.3.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4385,6 +4555,12 @@ } } }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -4846,6 +5022,16 @@ "@babel/runtime": "^7.3.4" } }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + } + }, "yargs": { "version": "12.0.5", "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", diff --git a/packages/cli/package.json b/packages/cli/package.json index 93dd7b594d06..ec33d04c128e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,7 +37,8 @@ "sinon": "^7.3.1", "yeoman-assert": "^3.1.1", "yeoman-environment": "^2.0.6", - "yeoman-test": "^1.7.0" + "yeoman-test": "^1.7.0", + "loopback-datasource-juggler": "^4.5.3" }, "dependencies": { "@phenomnomnominal/tsquery": "^3.0.0", diff --git a/packages/cli/test/fixtures/discover/index.js b/packages/cli/test/fixtures/discover/index.js new file mode 100644 index 000000000000..9135b633bff7 --- /dev/null +++ b/packages/cli/test/fixtures/discover/index.js @@ -0,0 +1,9 @@ +const fs = require('fs'); + +exports.SANDBOX_FILES = [ + { + path: 'dist/datasources', + file: 'mem.datasource.js', + content: fs.readFileSync(require.resolve('./mem.datasource.js.txt')), + }, +]; diff --git a/packages/cli/test/fixtures/discover/mem.datasource.js.txt b/packages/cli/test/fixtures/discover/mem.datasource.js.txt new file mode 100644 index 000000000000..f210eaddbd6e --- /dev/null +++ b/packages/cli/test/fixtures/discover/mem.datasource.js.txt @@ -0,0 +1,105 @@ +const DataSource = require('loopback-datasource-juggler').DataSource; + +const modelList = [ + { + name: 'Test', + view: false, + schema: '', + }, + { + name:'Schema', + view: false, + schema: 'aSchema' + }, + { + name:'View', + view: true, + schema: '' + } +]; +// In real model definitions, the schema is contained in options->connectorName->schema +const fullDefinitions = [ + { + 'name': 'Schema', + 'schema': 'aSchema', + 'properties': {} + }, + { + 'name': 'View', + 'view': true, + 'schema': '', + 'properties': {} + }, + { + 'name': 'Test', + 'properties': { + 'dateTest': { + 'type': 'Date', + 'required': false, + 'length': null, + 'precision': null, + 'scale': null, + }, + 'numberTest': { + 'type': 'Number', + 'required': false, + 'length': null, + 'precision': null, + 'scale': null, + }, + 'stringTest': { + 'type': 'String', + 'required': false, + 'length': null, + 'precision': null, + 'scale': null, + }, + 'booleanText': { + 'type': 'Boolean', + 'required': false, + 'length': null, + 'precision': null, + 'scale': null, + }, + 'id': { + 'type': 'Number', + 'required': true, + 'length': null, + 'precision': null, + 'scale': 0, + 'id': 1, + }, + }, + }, +]; + +class DiscoverOnly extends DataSource { + constructor() { + super(); + this.name = 'mem'; + this.connected = true; + } + + async discoverModelDefinitions(options = {views: true}) { + let models = modelList; + if (!options.views) { + models = models.filter(m => !m.view); + } + if (options.schema) { + models = models.filter(m => m.schema === options.schema); + } + + return models; + } + + async discoverSchema(name, options = {schema:''}) { + let fullDefs = fullDefinitions; + if (options.schema) { + fullDefs = fullDefs.filter(d => d.schema === options.schema); + } + return fullDefs.find(d => d.name === name); + } +} +module.exports = { + DiscoverOnly +}; \ No newline at end of file diff --git a/packages/cli/test/integration/cli/cli.integration.js b/packages/cli/test/integration/cli/cli.integration.js index 203a9fd7ce0c..27fbb691b10e 100644 --- a/packages/cli/test/integration/cli/cli.integration.js +++ b/packages/cli/test/integration/cli/cli.integration.js @@ -25,7 +25,7 @@ describe('cli', () => { 'Available commands: ', ' lb4 app\n lb4 extension\n lb4 controller\n lb4 datasource\n ' + 'lb4 model\n lb4 repository\n lb4 service\n lb4 example\n ' + - 'lb4 openapi\n lb4 observer', + 'lb4 openapi\n lb4 observer\n lb4 discover', ]); }); @@ -44,7 +44,7 @@ describe('cli', () => { expect(entries).to.containEql( ' lb4 app\n lb4 extension\n lb4 controller\n lb4 datasource\n ' + 'lb4 model\n lb4 repository\n lb4 service\n lb4 example\n ' + - 'lb4 openapi\n lb4 observer', + 'lb4 openapi\n lb4 observer\n lb4 discover', ); }); diff --git a/packages/cli/test/integration/generators/discover.integration.js b/packages/cli/test/integration/generators/discover.integration.js new file mode 100644 index 000000000000..ebd7888dad5d --- /dev/null +++ b/packages/cli/test/integration/generators/discover.integration.js @@ -0,0 +1,127 @@ +// 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'; + +// Imports +const path = require('path'); +const assert = require('yeoman-assert'); +const testlab = require('@loopback/testlab'); + +const {expect, TestSandbox} = testlab; + +const generator = path.join(__dirname, '../../../generators/discover'); +require('../lib/artifact-generator')(generator); +require('../lib/base-generator')(generator); +const testUtils = require('../../test-utils'); +const basicModelFileChecks = require('../lib/file-check').basicModelFileChecks; + +// Test Sandbox +const SANDBOX_PATH = path.resolve(__dirname, '../.sandbox'); +const SANDBOX_FILES = require('../../fixtures/discover').SANDBOX_FILES; +const sandbox = new TestSandbox(SANDBOX_PATH); + +// CLI Inputs +const baseOptions = { + all: true, + dataSource: 'mem', +}; +const outDirOptions = { + ...baseOptions, + outDir: 'src', +}; +const schemaViewsOptions = { + ...baseOptions, + schema: 'aSchema', + views: false, +}; +const missingDataSourceOptions = { + dataSource: 'foo', +}; + +// Expected File Name +const defaultExpectedTestModel = path.join( + SANDBOX_PATH, + 'src/models/test.model.ts', +); +const defaultExpectedSchemaModel = path.join( + SANDBOX_PATH, + 'src/models/schema.model.ts', +); +const defaultExpectedViewModel = path.join( + SANDBOX_PATH, + 'src/models/view.model.ts', +); + +const defaultExpectedIndexFile = path.join(SANDBOX_PATH, 'src/models/index.ts'); +const movedExpectedTestModel = path.join(SANDBOX_PATH, 'src/test.model.ts'); +const movedExpectedIndexFile = path.join(SANDBOX_PATH, 'src/index.ts'); + +// Base Tests +/*describe('discover-generator extending BaseGenerator', baseTests); +describe('generator-loopback4:discover', tests);*/ + +describe('lb4 discover integration', () => { + describe('model discovery', () => { + beforeEach('creates dist/datasources', async () => { + await sandbox.mkdir('dist/datasources'); + }); + beforeEach('reset sandbox', () => sandbox.reset()); + + it('generates all models without prompts using --all --dataSource', async function() { + this.timeout(10000); + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(baseOptions); + + basicModelFileChecks(defaultExpectedTestModel, defaultExpectedIndexFile); + assert.file(defaultExpectedSchemaModel); + assert.file(defaultExpectedViewModel); + }); + it('uses a different --outDir if provided', async () => { + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(outDirOptions); + + basicModelFileChecks(movedExpectedTestModel, movedExpectedIndexFile); + }); + it('excludes models based on the --views and --schema options', async () => { + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(schemaViewsOptions); + + assert.noFile(defaultExpectedViewModel); + assert.noFile(defaultExpectedTestModel); + assert.file(defaultExpectedSchemaModel); + }); + it('will fail gracefully if you specify a --dataSource which does not exist', async () => { + return expect( + testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: SANDBOX_FILES, + }), + ) + .withOptions(missingDataSourceOptions), + ).to.be.rejectedWith(/Cannot find datasource/); + }); + }); +}); diff --git a/packages/cli/test/integration/generators/model.integration.js b/packages/cli/test/integration/generators/model.integration.js index cb604f2e7b46..c73f668ebf37 100644 --- a/packages/cli/test/integration/generators/model.integration.js +++ b/packages/cli/test/integration/generators/model.integration.js @@ -17,9 +17,11 @@ const generator = path.join(__dirname, '../../../generators/model'); const tests = require('../lib/artifact-generator')(generator); const baseTests = require('../lib/base-generator')(generator); const testUtils = require('../../test-utils'); +const basicModelFileChecks = require('../lib/file-check').basicModelFileChecks; // Test Sandbox const SANDBOX_PATH = path.resolve(__dirname, '../.sandbox'); +const DISCOVER_SANDBOX_FILES = require('../../fixtures/discover').SANDBOX_FILES; const sandbox = new TestSandbox(SANDBOX_PATH); // Basic CLI Input @@ -80,6 +82,30 @@ describe('lb4 model integration', () => { assert.file(expectedModelFile); }); + it('will discover a model through a datasource', async () => { + await testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: DISCOVER_SANDBOX_FILES, + }), + ) + .withArguments('--dataSource mem --table Test'); + assert.file(expectedModelFile); + }); + it('will fail gracefully if datasource discovery does not find the model ', async () => { + return expect( + testUtils + .executeGenerator(generator) + .inDir(SANDBOX_PATH, () => + testUtils.givenLBProject(SANDBOX_PATH, { + additionalFiles: DISCOVER_SANDBOX_FILES, + }), + ) + .withArguments('--dataSource mem --table Foo'), + ).to.be.rejectedWith(/Could not locate table:/); + }); + describe('model generator', () => { it('scaffolds correct files with input', async () => { await testUtils @@ -90,7 +116,7 @@ describe('lb4 model integration', () => { propName: null, }); - basicModelFileChecks(); + basicModelFileChecks(expectedModelFile, expectedIndexFile); }); it('scaffolds correct files with model base class', async () => { @@ -184,7 +210,7 @@ describe('lb4 model integration', () => { propName: null, }); - basicModelFileChecks(); + basicModelFileChecks(expectedModelFile, expectedIndexFile); }); }); }); @@ -196,7 +222,7 @@ describe('model generator using --config option', () => { .inDir(SANDBOX_PATH, () => testUtils.givenLBProject(SANDBOX_PATH)) .withArguments(['--config', '{"name":"test", "base":"Entity"}', '--yes']); - basicModelFileChecks(); + basicModelFileChecks(expectedModelFile, expectedIndexFile); }); it('does not run if pass invalid json', () => { @@ -212,25 +238,3 @@ describe('model generator using --config option', () => { ).to.be.rejectedWith(/Model was not found in/); }); }); - -// Checks to ensure expected files exist with the current file contents -function basicModelFileChecks() { - assert.file(expectedModelFile); - assert.file(expectedIndexFile); - - // Actual Model File - assert.fileContent( - expectedModelFile, - /import {Entity, model, property} from '@loopback\/repository';/, - ); - assert.fileContent(expectedModelFile, /@model()/); - assert.fileContent(expectedModelFile, /export class Test extends Entity {/); - assert.fileContent( - expectedModelFile, - /constructor\(data\?\: Partial\) {/, - ); - assert.fileContent(expectedModelFile, /super\(data\)/); - - // Actual Index File - assert.fileContent(expectedIndexFile, /export \* from '.\/test.model';/); -} diff --git a/packages/cli/test/integration/lib/file-check.js b/packages/cli/test/integration/lib/file-check.js new file mode 100644 index 000000000000..d8455b4ba82a --- /dev/null +++ b/packages/cli/test/integration/lib/file-check.js @@ -0,0 +1,27 @@ +const assert = require('yeoman-assert'); + +// Checks to ensure expected files exist with the current file contents +function basicModelFileChecks(expectedModelFile, expectedIndexFile) { + assert.file(expectedModelFile); + assert.file(expectedIndexFile); + + // Actual Model File + assert.fileContent( + expectedModelFile, + /import {Entity, model, property} from '@loopback\/repository';/, + ); + assert.fileContent(expectedModelFile, /@model/); + assert.fileContent(expectedModelFile, /export class Test extends Entity {/); + assert.fileContent( + expectedModelFile, + /constructor\(data\?\: Partial\) {/, + ); + assert.fileContent(expectedModelFile, /super\(data\)/); + + // Actual Index File + assert.fileContent(expectedIndexFile, /export \* from '.\/test.model';/); +} + +module.exports = { + basicModelFileChecks, +}; diff --git a/packages/cli/test/test-utils.js b/packages/cli/test/test-utils.js index 014979f7c789..a3d49d8a0641 100644 --- a/packages/cli/test/test-utils.js +++ b/packages/cli/test/test-utils.js @@ -2,13 +2,12 @@ // Node module: @loopback/cli // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT - 'use strict'; const yeoman = require('yeoman-environment'); const path = require('path'); const helpers = require('yeoman-test'); -const fs = require('fs'); +const fs = require('fs-extra'); exports.testSetUpGen = function(genName, arg) { arg = arg || {}; @@ -125,6 +124,7 @@ exports.givenLBProject = function(rootDir, options) { for (let theFile of sandBoxFiles) { const fullPath = path.join(rootDir, theFile.path, theFile.file); if (!fs.existsSync(fullPath)) { + fs.ensureDirSync(path.dirname(fullPath)); fs.writeFileSync(fullPath, theFile.content); } }