-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(cli): generate controller with CRUD methods #849
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,25 +4,56 @@ | |
| // License text available at https://opensource.org/licenses/MIT | ||
|
|
||
| 'use strict'; | ||
| const _ = require('lodash'); | ||
| const ArtifactGenerator = require('../../lib/artifact-generator'); | ||
| const debug = require('../../lib/debug')('controller-generator'); | ||
| const inspect = require('util').inspect; | ||
| const path = require('path'); | ||
| const utils = require('../../lib/utils'); | ||
|
|
||
| // Exportable constants | ||
| module.exports = class ControllerGenerator extends ArtifactGenerator { | ||
| // Note: arguments and options should be defined in the constructor. | ||
| constructor(args, opts) { | ||
| super(args, opts); | ||
| } | ||
|
|
||
| static get BASIC() { | ||
| return 'Empty Controller'; | ||
| } | ||
|
|
||
| static get REST() { | ||
| return 'REST Controller with CRUD functions'; | ||
| } | ||
|
|
||
| _setupGenerator() { | ||
| this.artifactInfo = { | ||
| type: 'controller', | ||
| outdir: 'src/controllers/', | ||
| rootDir: 'src', | ||
| }; | ||
| if (debug.enabled) { | ||
| debug(`artifactInfo: ${inspect(this.artifactInfo)}`); | ||
| } | ||
|
|
||
| // XXX(kjdelisle): These should be more extensible to allow custom paths | ||
| // for each artifact type. | ||
|
|
||
| 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' | ||
| ); | ||
|
|
||
| this.option('controllerType', { | ||
| type: String, | ||
| required: false, | ||
| description: 'Type for the ' + this.artifactInfo.type, | ||
| }); | ||
|
|
||
| return super._setupGenerator(); | ||
| } | ||
|
|
||
|
|
@@ -34,25 +65,154 @@ module.exports = class ControllerGenerator extends ArtifactGenerator { | |
| return super.promptArtifactName(); | ||
| } | ||
|
|
||
| promptArtifactType() { | ||
| debug('Prompting for controller type'); | ||
| return this.prompt([ | ||
| { | ||
| type: 'list', | ||
| name: 'controllerType', | ||
| message: 'What kind of controller would you like to generate?', | ||
| when: this.artifactInfo.controllerType === undefined, | ||
| choices: [ControllerGenerator.BASIC, ControllerGenerator.REST], | ||
| default: ControllerGenerator.BASIC, | ||
| }, | ||
| ]) | ||
| .then(props => { | ||
| Object.assign(this.artifactInfo, props); | ||
| return props; | ||
| }) | ||
| .catch(err => { | ||
| debug(`Error during controller type prompt: ${err.stack}`); | ||
| return this.exit(err); | ||
| }); | ||
| } | ||
|
|
||
| promptArtifactCrudVars() { | ||
| let modelList = []; | ||
| let repositoryList = []; | ||
| if ( | ||
| !this.artifactInfo.controllerType || | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm glad you noticed. I wrote it this way since I'll be adding the REST CRUD Controller afterwards. If in the future we have variance between which controllers do and don't require models/repositories/other artifacts, then we'll need to implement some sort of strategy pattern here to determine requirements |
||
| this.artifactInfo.controllerType === ControllerGenerator.BASIC | ||
| ) { | ||
| return; | ||
| } | ||
| return utils | ||
| .getArtifactList(this.artifactInfo.modelDir, 'model') | ||
| .then(list => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a .catch at the end of the chain to call
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately it does not work as your added |
||
| if (_.isEmpty(list)) { | ||
| return Promise.reject( | ||
| new Error(`No models found in ${this.artifactInfo.modelDir}`) | ||
| ); | ||
| } | ||
| modelList = list; | ||
| return utils.getArtifactList( | ||
| this.artifactInfo.repositoryDir, | ||
| 'repository', | ||
| true | ||
| ); | ||
| }) | ||
| .then(list => { | ||
| if (_.isEmpty(list)) { | ||
| return Promise.reject( | ||
| new Error( | ||
| `No repositories found in ${this.artifactInfo.repositoryDir}` | ||
| ) | ||
| ); | ||
| } | ||
| repositoryList = list; | ||
| return this.prompt([ | ||
| { | ||
| type: 'list', | ||
| name: 'modelName', | ||
| message: | ||
| 'What is the name of the model to use with this CRUD repository?', | ||
| choices: modelList, | ||
| when: this.artifactInfo.modelName === undefined, | ||
| default: modelList[0], | ||
| validate: utils.validateClassName, | ||
| }, | ||
| { | ||
| type: 'list', | ||
| name: 'repositoryName', | ||
| message: 'What is the name of your CRUD repository?', | ||
| choices: repositoryList, | ||
| when: this.artifactInfo.repositoryName === undefined, | ||
| default: repositoryList[0], | ||
| validate: utils.validateClassName, | ||
| }, | ||
| { | ||
| type: 'list', | ||
| name: 'idType', | ||
| message: 'What is the type of your ID?', | ||
| choices: ['number', 'string', 'object'], | ||
| when: this.artifactInfo.idType === undefined, | ||
| default: 'number', | ||
| }, | ||
| ]).then(props => { | ||
| debug(`props: ${inspect(props)}`); | ||
| Object.assign(this.artifactInfo, props); | ||
| // Ensure that the artifact names are valid. | ||
| [ | ||
| this.artifactInfo.name, | ||
| this.artifactInfo.modelName, | ||
| this.artifactInfo.repositoryName, | ||
| ].forEach(item => { | ||
| item = utils.toClassName(item); | ||
| }); | ||
| // Create camel-case names for variables. | ||
| this.artifactInfo.repositoryNameCamel = utils.camelCase( | ||
| this.artifactInfo.repositoryName | ||
| ); | ||
| this.artifactInfo.modelNameCamel = utils.camelCase( | ||
| this.artifactInfo.modelName | ||
| ); | ||
| return props; | ||
| }); | ||
| }) | ||
| .catch(err => { | ||
| debug(`Error during prompt for controller variables: ${err}`); | ||
| return this.exit(err); | ||
| }); | ||
| } | ||
|
|
||
| scaffold() { | ||
| super.scaffold(); | ||
| // We don't want to call the base scaffold function since it copies | ||
| // all of the templates! | ||
| if (this.shouldExit()) return false; | ||
| this.artifactInfo.name = utils.toClassName(this.artifactInfo.name); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessary given we did it in lines 155-160 above? (https://github.com/strongloop/loopback-next/pull/849/files#diff-3195cd2d1a3887230542f8d33260e203R160)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, since that step is not guaranteed to run (basic controllers). |
||
| this.artifactInfo.filename = | ||
| utils.kebabCase(this.artifactInfo.name) + '.controller.ts'; | ||
| if (debug.enabled) { | ||
| debug(`Artifact filename set to: ${this.artifactInfo.filename}`); | ||
| } | ||
| // renames the file | ||
| const source = this.destinationPath( | ||
| this.artifactInfo.outdir + 'controller-template.ts' | ||
| ); | ||
| let template = 'controller-template.ts'; | ||
| switch (this.artifactInfo.controllerType) { | ||
| case ControllerGenerator.REST: | ||
| template = 'controller-rest-template.ts'; | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| const source = this.templatePath(path.join('src', 'controllers', template)); | ||
| if (debug.enabled) { | ||
| debug(`Using template at: ${source}`); | ||
| } | ||
| const dest = this.destinationPath( | ||
| this.artifactInfo.outdir + this.artifactInfo.filename | ||
| path.join(this.artifactInfo.outdir, this.artifactInfo.filename) | ||
| ); | ||
|
|
||
| if (debug.enabled) { | ||
| debug(`artifactInfo: ${inspect(this.artifactInfo)}`); | ||
| debug(`Copying artifact to: ${dest}`); | ||
| } | ||
| this.fs.move(source, dest, {globOptions: {dot: true}}); | ||
| this.fs.copyTpl( | ||
| source, | ||
| dest, | ||
| this.artifactInfo, | ||
| {}, | ||
| {globOptions: {dot: true}} | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import {Filter, Where} from '@loopback/repository'; | ||
| import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; | ||
| import {inject} from '@loopback/context'; | ||
| import {<%= modelName %>} from '../models'; | ||
| import {<%= repositoryName %>} from '../repositories'; | ||
|
|
||
| export class <%= name %>Controller { | ||
|
|
||
| constructor( | ||
| @inject('repositories.<%= repositoryName %>') | ||
| public <%= repositoryNameCamel %> : <%= repositoryName %>, | ||
| ) {} | ||
|
|
||
| @post('/<%= modelNameCamel %>') | ||
| async create(@param.body('obj') obj: <%= modelName %>) | ||
| : Promise<<%= modelName %>> { | ||
| return await this.<%= repositoryNameCamel %>.create(obj); | ||
| } | ||
|
|
||
| @get('/<%= modelNameCamel %>/count') | ||
| async count(@param.query.string('where') where: Where) : Promise<number> { | ||
| return await this.<%= repositoryNameCamel %>.count(where); | ||
| } | ||
|
|
||
| @get('/<%= modelNameCamel %>') | ||
| async find(@param.query.string('filter') filter: Filter) | ||
| : Promise<<%= modelName %>[]> { | ||
| return await this.<%= repositoryNameCamel %>.find(filter); | ||
| } | ||
|
|
||
| @patch('/<%= modelNameCamel %>') | ||
| async updateAll(@param.query.string('where') where: Where, | ||
| @param.body('obj') obj: <%= modelName %>) : Promise<number> { | ||
| return await this.<%= repositoryNameCamel %>.updateAll(where, obj); | ||
| } | ||
|
|
||
| @del('/<%= modelNameCamel %>') | ||
| async deleteAll(@param.query.string('where') where: Where) : Promise<number> { | ||
| return await this.<%= repositoryNameCamel %>.deleteAll(where); | ||
| } | ||
|
|
||
| @get('/<%= modelNameCamel %>/{id}') | ||
| async findById(@param.path.number('id') id: <%= idType %>) : Promise<<%= modelName %>> { | ||
| return await this.<%= repositoryNameCamel %>.findById(id); | ||
| } | ||
|
|
||
| @patch('/<%= modelNameCamel %>/{id}') | ||
| async updateById(@param.path.number('id') id: <%= idType %>, @param.body('obj') | ||
| obj: <%= modelName %>) : Promise<boolean> { | ||
| return await this.<%= repositoryNameCamel %>.updateById(id, obj); | ||
| } | ||
|
|
||
| @del('/<%= modelNameCamel %>/{id}') | ||
| async deleteById(@param.path.number('id') id: <%= idType %>) : Promise<boolean> { | ||
| return await this.<%= repositoryNameCamel %>.deleteById(id); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this debug statement still be helpful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the code itself moved to a different line.