-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(cli): add lb4 relation command #2426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| --- | ||
| lang: en | ||
| title: 'Relation generator' | ||
| keywords: LoopBack 4.0, LoopBack 4 | ||
| sidebar: lb4_sidebar | ||
| permalink: /doc/en/lb4/Relation-generator.html | ||
| --- | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| Important: Before running this generator, make sure the models, datasource, and | ||
| repositories involved in this relation exist. Then, inside your LoopBack | ||
| application, run the command from the root directory. | ||
|
|
||
| {% include content/generator-create-app.html lang=page.lang %} | ||
|
|
||
| ### Synopsis | ||
|
|
||
| Adds a new `Relation` between existing source and target models in a LoopBack | ||
| application. | ||
|
|
||
| ```sh | ||
| lb4 relation [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`. | ||
| - `--relationType`: Relation type. | ||
| - `--sourceModel`: Source model. | ||
| - `--destinationModel`: Destination model. | ||
| - `--foreignKeyName`: Destination model foreign key name. | ||
| - `--relationName`: Relation name. | ||
| - `-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`. | ||
|
|
||
| ### Arguments | ||
|
|
||
| Defining lb4 relation in one command line interface (cli): | ||
|
|
||
| ```sh | ||
| lb4 relation --sourceModel=<sourceModel> | ||
| --destinationModel=<destinationModel> --foreignKeyName=<foreignKeyName> | ||
| --relationType=<hasMany|belongsTo> [--relationName=<relationName>] [--format] | ||
| ``` | ||
|
|
||
| - `<relationType>` - Type of the relation that will be created between the | ||
| source and target models. | ||
|
|
||
| - `<sourceModel>` - Name of the model to create the relationship from. | ||
|
|
||
| - `<destinationModel>` - Name of the model to create a relationship with. | ||
|
|
||
| - `<foreignKeyName>` - Property that references the primary key property of the | ||
| destination model. | ||
|
|
||
| - `<relationName>` - Name of the relation that will be created. | ||
|
|
||
| ### Interactive Prompts | ||
|
|
||
| The tool will prompt you for: | ||
|
|
||
| - **Relation `type` between models.** _(relationBaseClass)_ Prompts a list of | ||
| available relations to choose from as the type of the relation between the | ||
| source model and the target model. Supported relation types: | ||
|
|
||
| - [HasMany](HasMany-relation.md) | ||
| - [BelongsTo](BelongsTo-relation.md) | ||
|
|
||
| - **Name of the `source` model.** _(sourceModel)_ Prompts a list of available | ||
| models to choose from as the source model of the relation. | ||
|
|
||
| - **Name of the `target` model.** _(targetModel)_ Prompts a list of available | ||
oleg269 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| models to choose from as the target model of the relation. | ||
|
|
||
| - **Name of the `Source property`.** _(relationName)_ Prompts for the Source | ||
| property name. Note: Leave blank to use the default. | ||
|
|
||
| Default values: | ||
|
|
||
| - `<targetModel><targetModelPrimaryKey>` for `belongsTo` relations, e.g. | ||
| `categoryId` | ||
| - plural form of `<targetModel>` for `hasMany` relations, e.g. `products` | ||
|
|
||
| - **Name of Foreign key** _(foreignKeyName)_ to be created in target model. For | ||
| hasMany relation type only, default: `<sourceModel><sourceModelPrimaryKey>`. | ||
| Note: Leave blank to use the default. | ||
|
|
||
| ### Output | ||
|
|
||
| Once all the prompts have been answered, the CLI will update or create source | ||
| files for the Entities involved in the relation. | ||
|
|
||
| - Update source Model class as follows: | ||
| `/src/models/${sourceModel-Name}.model.ts` | ||
| - Update target Model class as follows: | ||
| `/src/models/${targetModel-Name}.model.ts` | ||
| - Update source Model Repository class as follows: | ||
| `/src/repositories/${sourceModel-Repository-Name}.repository.ts` | ||
| - Update target Model Repository class as follows: | ||
| `/src/repositories/${targetModel-Repository-Name}.repository.ts` | ||
| - Create a Controller for the new relation as follows: | ||
| `/src/controllers/{sourceModel-Name}-{targetModel-Name}.controller.ts` | ||
| - Update `/src/controllers/index.ts` to export the newly created Controller | ||
| class. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
packages/cli/generators/relation/base-relation.generator.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| // Copyright IBM Corp. 2019. All Rights Reserved. | ||
| // Node module: @loopback/cli | ||
| // This file is licensed under the MIT License. | ||
| // License text available at https://opensource.org/licenses/MIT | ||
|
|
||
| 'use strict'; | ||
|
|
||
| const ast = require('ts-morph'); | ||
| const ArtifactGenerator = require('../../lib/artifact-generator'); | ||
| const path = require('path'); | ||
| const relationUtils = require('./utils.generator'); | ||
| const utils = require('../../lib/utils'); | ||
|
|
||
| module.exports = class BaseRelationGenerator extends ArtifactGenerator { | ||
| constructor(args, opts) { | ||
| super(args, opts); | ||
| } | ||
|
|
||
| _setupGenerator() { | ||
| this.artifactInfo = { | ||
| type: 'relation', | ||
| rootDir: utils.sourceRootDir, | ||
| }; | ||
|
|
||
| this.artifactInfo.outDir = path.resolve( | ||
| this.artifactInfo.rootDir, | ||
| 'controllers', | ||
| ); | ||
| this.artifactInfo.modelDir = path.resolve( | ||
| this.artifactInfo.rootDir, | ||
| 'models', | ||
| ); | ||
| this.artifactInfo.repositoryDir = path.resolve( | ||
| this.artifactInfo.rootDir, | ||
| 'repositories', | ||
| ); | ||
|
|
||
| super._setupGenerator(); | ||
| } | ||
|
|
||
| async generateAll(options) { | ||
| this._setupGenerator(); | ||
| await this.generateControllers(options); | ||
| this._setupGenerator(); | ||
| await this.generateModels(options); | ||
| this._setupGenerator(); | ||
| await this.generateRepositories(options); | ||
| } | ||
|
|
||
| generateControllers(options) { | ||
| /* istanbul ignore next */ | ||
| throw new Error('Not implemented'); | ||
| } | ||
|
|
||
| generateModels(options) { | ||
| /* istanbul ignore next */ | ||
| throw new Error('Not implemented'); | ||
| } | ||
|
|
||
| async generateRepositories(options) { | ||
| this._initializeProperties(options); | ||
| this._addImportsToRepository(options); | ||
| this._addPropertyToRepository(options); | ||
| const classDeclaration = relationUtils.getClassObj( | ||
| this.artifactInfo.srcRepositoryFileObj, | ||
| this.artifactInfo.srcRepositoryClassName, | ||
| ); | ||
| const classConstructor = relationUtils.getClassConstructor( | ||
| classDeclaration, | ||
| ); | ||
| this._addParametersToRepositoryConstructor(classConstructor); | ||
| this._addCreatorToRepositoryConstructor(classConstructor); | ||
| await this.artifactInfo.srcRepositoryFileObj.save(); | ||
| } | ||
|
|
||
| _addImportsToRepository(options) { | ||
| const imports = this._getRepositoryRequiredImports( | ||
| options.destinationModel, | ||
| this.artifactInfo.dstRepositoryClassName, | ||
| ); | ||
|
|
||
| relationUtils.addRequiredImports( | ||
| this.artifactInfo.srcRepositoryFileObj, | ||
| imports, | ||
| ); | ||
| } | ||
|
|
||
| _addPropertyToRepository(options) { | ||
| const classDeclaration = this.artifactInfo.srcRepositoryFileObj.getClassOrThrow( | ||
| this.artifactInfo.srcRepositoryClassName, | ||
| ); | ||
|
|
||
| const property = { | ||
| scope: ast.Scope.Public, | ||
| isReadonly: true, | ||
| name: this._getRepositoryRelationPropertyName(), | ||
| type: this._getRepositoryRelationPropertyType(), | ||
| }; | ||
|
|
||
| if (relationUtils.doesPropertyExist(classDeclaration, property.name)) { | ||
| throw new Error( | ||
| 'property ' + property.name + ' already exist in the repository.', | ||
| ); | ||
| } else { | ||
| relationUtils.addProperty(classDeclaration, property); | ||
| } | ||
| } | ||
|
|
||
| _addParametersToRepositoryConstructor(classConstructor) { | ||
| const parameterName = | ||
| utils.camelCase(this.artifactInfo.dstRepositoryClassName) + 'Getter'; | ||
|
|
||
| if (relationUtils.doesParameterExist(classConstructor, parameterName)) { | ||
| throw new Error( | ||
| 'Parameter ' + parameterName + ' already exist in the constructor.', | ||
| ); | ||
| } | ||
|
|
||
| classConstructor.addParameter({ | ||
| decorators: [ | ||
| { | ||
| name: 'repository.getter', | ||
| arguments: ["'" + this.artifactInfo.dstRepositoryClassName + "'"], | ||
| }, | ||
| ], | ||
| name: parameterName, | ||
| type: 'Getter<' + this.artifactInfo.dstRepositoryClassName + '>,', | ||
| scope: ast.Scope.Protected, | ||
| }); | ||
| } | ||
|
|
||
| _addCreatorToRepositoryConstructor(classConstructor) { | ||
| /* istanbul ignore next */ | ||
| throw new Error('Not implemented'); | ||
| } | ||
|
|
||
| _initializeProperties(options) { | ||
| // src configuration. | ||
| this.artifactInfo.srcModelPrimaryKey = options.sourceModelPrimaryKey; | ||
| this.artifactInfo.srcModelFile = path.resolve( | ||
| this.artifactInfo.modelDir, | ||
| utils.getModelFileName(options.sourceModel), | ||
| ); | ||
|
|
||
| this.artifactInfo.srcModelClass = options.sourceModel; | ||
|
|
||
| this.artifactInfo.srcRepositoryFile = path.resolve( | ||
| this.artifactInfo.repositoryDir, | ||
| utils.getRepositoryFileName(options.sourceModel), | ||
| ); | ||
|
|
||
| this.artifactInfo.srcRepositoryClassName = | ||
| utils.toClassName(options.sourceModel) + 'Repository'; | ||
|
|
||
| this.artifactInfo.srcRepositoryFileObj = new relationUtils.AstLoopBackProject().addExistingSourceFile( | ||
| this.artifactInfo.srcRepositoryFile, | ||
| ); | ||
|
|
||
| // dst configuration | ||
| this.artifactInfo.dstModelFile = path.resolve( | ||
| this.artifactInfo.modelDir, | ||
| utils.getModelFileName(options.destinationModel), | ||
| ); | ||
|
|
||
| this.artifactInfo.dstModelClass = options.destinationModel; | ||
|
|
||
| this.artifactInfo.dstRepositoryFile = path.resolve( | ||
| this.artifactInfo.repositoryDir, | ||
| utils.getRepositoryFileName(options.destinationModel), | ||
| ); | ||
|
|
||
| this.artifactInfo.dstRepositoryClassName = | ||
| utils.toClassName(options.destinationModel) + 'Repository'; | ||
|
|
||
| // relation configuration | ||
| this.artifactInfo.relationName = options.relationName; | ||
| } | ||
|
|
||
| _getRepositoryRequiredImports(dstModelClassName, dstRepositoryClassName) { | ||
| return [ | ||
| { | ||
| name: dstModelClassName, | ||
| module: '../models', | ||
| }, | ||
| { | ||
| name: 'repository', | ||
| module: '@loopback/repository', | ||
| }, | ||
| { | ||
| name: 'Getter', | ||
| module: '@loopback/core', | ||
| }, | ||
| { | ||
| name: dstRepositoryClassName, | ||
| module: `./${utils.kebabCase(dstModelClassName)}.repository`, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| _getRepositoryRelationPropertyName() { | ||
| /* istanbul ignore next */ | ||
| throw new Error('Not implemented'); | ||
oleg269 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| _getRepositoryRelationPropertyType() { | ||
| /* istanbul ignore next */ | ||
| throw new Error('Not implemented'); | ||
bajtos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.