Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ Options:

```

3. To scaffold a controller into your application

```sh
cd <your-project-directory>
lb4 controller
```

```
Usage:
lb4 controller [options] [<name>]

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
--controllerType # Type for the controller

Arguments:
name # Name for the controller Type: String Required: false
```

# Tests

run `npm test` from the root folder.
Expand Down
180 changes: 170 additions & 10 deletions packages/cli/generators/controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

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

Expand All @@ -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 ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, is promptArtifactCrudVars for "Basic CRUD controller" only or for any other controllers except artifactInfo.controllerType === ControllerGenerator.BASIC?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add catch here so that errors from getArtifactList will be caught for graceful exit. For example, if there is no models directory, running the generator throws the following error:

events.js:183
      throw er; // Unhandled 'error' event
      ^

Error: ENOENT: no such file or directory, scandir '/Users/rfeng/Projects/demos/x2/src/models'

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 this.exit (it won't mark this comment as outdated)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it does not work as your added catch only applies to return this.prompt([. I still see the same issue with your changes.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Expand Down
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);
}
}
1 change: 1 addition & 0 deletions packages/cli/lib/artifact-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ module.exports = class ArtifactGenerator extends BaseGenerator {

return this.prompt(prompts).then(props => {
Object.assign(this.artifactInfo, props);
return props;
});
}

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/lib/base-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ module.exports = class BaseGenerator extends Generator {
* Subclasses can extend _setupGenerator() to set up the generator
*/
_setupGenerator() {
// No operation
this.artifactInfo = {
rootDir: 'src',
};
}

/**
Expand Down
Loading