Skip to content

Commit

Permalink
Merge 8e75d0b into 596a143
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinirwin committed Feb 9, 2019
2 parents 596a143 + 8e75d0b commit 6243c6c
Show file tree
Hide file tree
Showing 18 changed files with 750 additions and 29 deletions.
49 changes: 49 additions & 0 deletions docs/site/Discovering-models-from-relational-databases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
lang: en
title: 'Discovering models from relational databases'
keywords: LoopBack 4.0, LoopBack-Next
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Discovering-models-from-relational-databases.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](Cassandra-connector.html)
- [MySQL](MySQL-connector.html)
- [Oracle](Oracle-connector.html)
- [PostgreSQL](PostgreSQL-connector.html)
- [SQL Server](SQL-Server-connector.html)
- [IBM DB2](DB2-connector.html)
- [IBM DashDB](DashDB.html)
- [IBM DB2 for z/OS](DB2-for-z-OS.html)
- [SAP HANA](https://www.npmjs.org/package/loopback-connector-saphana) - Not
officially supported; see [Community connectors](Community-connectors.html).

## 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/src/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

`--schema`: Specify the schema which the datasource will find the models to
discover

`--schemaDefs`: The path to a `.json` file containing a model definition, like
those used loopback 2 and 3. LoopBack 4 models will be created out of the
LoopBack 3 models specified in `.json` file without any prompts.
5 changes: 5 additions & 0 deletions docs/site/Model.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ decorators for adding metadata to your TypeScript/JavaScript classes in order to
use them with the legacy implementation of the
[datasource juggler](https://github.com/strongloop/loopback-datasource-juggler).

## Model Discovery

Some connectors support the ability to
[Discover](Discovering-models-from-relational-databases.md) models.

## Definition of a Model

At its core, a model in LoopBack is a simple JavaScript class.
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 @@ -269,6 +269,10 @@ children:
url: Model-generator.html
output: 'web, pdf'

- title: 'Model discovery'
url: Discovering-models-from-relational-databases.html
output: 'web, pdf'

- title: 'Repository generator'
url: Repository-generator.html
output: 'web, pdf'
Expand Down
6 changes: 6 additions & 0 deletions docs/site/tables/lb4-artifact-commands.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,11 @@
<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-from-relational-databases.html">Model Discovery</a></td>
</tr>

</tbody>
</table>
279 changes: 279 additions & 0 deletions packages/cli/generators/discover/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
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');

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('schemaDefs', {
type: String,
description:
'The path to a .json file with an array of schema definition',
default: undefined,
});
}

_setupGenerator() {
this.artifactInfo = {
type: 'discover',
rootDir: 'src',
};

this.artifactInfo.outDir = path.resolve(
this.artifactInfo.rootDir,
'models',
);

return super._setupGenerator();
}

/**
* If we have a dataSource, attempt to load it
* If we have a schemaDef path, 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,
);
}

if (this.options.schemaDefs) {
this.artifactInfo.modelDefinitions = JSON.parse(
fs.readFileSync(this.options.schemaDefs).toString(),
);
// If the model definition object is not an array, assume it's a single definition, and make it an array
if (!Array.isArray(this.artifactInfo.modelDefinitions)) {
this.artifactInfo.modelDefinitions = [
this.artifactInfo.modelDefinitions,
];
}
}

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
*/
loadAllDatasources() {
// If we have a dataSourcePath then it is already loaded for us, we don't need load any
if (this.artifactInfo.dataSource || this.artifactInfo.modelDefinitions) {
debug(
`Not loading all data sources. dataSource: ${this.artifactInfo
.dataSource &&
this.artifactInfo.dataSource.name}, modelDefinitions count: ${this
.artifactInfo.modelDefinitions &&
this.artifactInfo.modelDefinitions.length}`,
);
return;
}

debug(
`Importing all datasources from ${
modelDiscoverer.DEFAULT_DATASOURCE_DIRECTORY
}`,
);
const datasourceFiles = fs
.readdirSync(modelDiscoverer.DEFAULT_DATASOURCE_DIRECTORY)
.filter(s => s.endsWith('.datasource.js'));
debug(`datasources: ${JSON.stringify(datasourceFiles)}`);
this.dataSourceChoices = datasourceFiles.map(s =>
modelDiscoverer.loadDataSource(
path.resolve(modelDiscoverer.DEFAULT_DATASOURCE_DIRECTORY, s),
),
);
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() {
if (this.artifactInfo.modelDefinitions) return;
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,
{schema: modelInfo.schema},
modelInfo.name,
),
);
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}`);

debug(`Writing: ${modelDefinition.name}`);
const fullPath = path.resolve(
this.artifactInfo.outDir,
utils.getModelFileName(modelDefinition.name),
);

this.copyTemplatedFiles(
modelDiscoverer.MODEL_TEMPLATE_PATH,
fullPath,
modelDefinition,
);
this.artifactInfo.indexesToBeUpdated.push({
dir: 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();
}
};

0 comments on commit 6243c6c

Please sign in to comment.