Skip to content

Commit

Permalink
feat(cli): add lb4 discover for model discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
frodo authored and b-admike committed Apr 10, 2019
1 parent 8b5c63f commit 35f719c
Show file tree
Hide file tree
Showing 20 changed files with 1,042 additions and 36 deletions.
45 changes: 45 additions & 0 deletions docs/site/Discovering-models.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions docs/site/Model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/site/sidebars/lb4_sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 7 additions & 0 deletions docs/site/tables/lb4-artifact-commands.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,18 @@
<td><a href="OpenAPI-generator.html">OpenAPI generator</a></td>
</tr>

<tr>
<td><code>lb4 discover</code></td>
<td>Discover models from relational databases</td>
<td><a href="Discovering-models.html">Model Discovery</a></td>
</tr>

<tr>
<td><code>lb4 observer</code></td>
<td>Generate life cycle observers for application start/stop</td>
<td><a href="Life-cycle-observer-generator.html">Life cycle observer generator</a></td>
</tr>


</tbody>
</table>
30 changes: 28 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <your-project-directory>
lb4 discover
lb4 discover [<name>] [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`)

Expand All @@ -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`)

Expand Down
255 changes: 255 additions & 0 deletions packages/cli/generators/discover/index.js
Original file line number Diff line number Diff line change
@@ -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<void>}
*/
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();
}
};
Loading

0 comments on commit 35f719c

Please sign in to comment.