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);
}
}