Skip to content

Commit 57fe858

Browse files
author
Kevin Delisle
committed
feat(cli): generate REST controller with CRUD methods
1 parent 4286c0d commit 57fe858

File tree

10 files changed

+847
-72
lines changed

10 files changed

+847
-72
lines changed

packages/cli/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,27 @@ Options:
5656
5757
```
5858

59+
3. To scaffold a controller into your application
60+
61+
```sh
62+
cd <your-project-directory>
63+
lb4 controller
64+
```
65+
66+
```
67+
Usage:
68+
lb4 controller [options] [<name>]
69+
70+
Options:
71+
-h, --help # Print the generator's options and usage
72+
--skip-cache # Do not remember prompt answers Default: false
73+
--skip-install # Do not automatically install dependencies Default: false
74+
--controllerType # Type for the controller
75+
76+
Arguments:
77+
name # Name for the controller Type: String Required: false
78+
```
79+
5980
# Tests
6081

6182
run `npm test` from the root folder.

packages/cli/generators/controller/index.js

Lines changed: 170 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,56 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
'use strict';
7+
const _ = require('lodash');
78
const ArtifactGenerator = require('../../lib/artifact-generator');
89
const debug = require('../../lib/debug')('controller-generator');
910
const inspect = require('util').inspect;
11+
const path = require('path');
1012
const utils = require('../../lib/utils');
1113

14+
// Exportable constants
1215
module.exports = class ControllerGenerator extends ArtifactGenerator {
1316
// Note: arguments and options should be defined in the constructor.
1417
constructor(args, opts) {
1518
super(args, opts);
1619
}
1720

21+
static get BASIC() {
22+
return 'Empty Controller';
23+
}
24+
25+
static get REST() {
26+
return 'REST Controller with CRUD functions';
27+
}
28+
1829
_setupGenerator() {
1930
this.artifactInfo = {
2031
type: 'controller',
21-
outdir: 'src/controllers/',
32+
rootDir: 'src',
2233
};
23-
if (debug.enabled) {
24-
debug(`artifactInfo: ${inspect(this.artifactInfo)}`);
25-
}
34+
35+
// XXX(kjdelisle): These should be more extensible to allow custom paths
36+
// for each artifact type.
37+
38+
this.artifactInfo.outdir = path.resolve(
39+
this.artifactInfo.rootDir,
40+
'controllers'
41+
);
42+
this.artifactInfo.modelDir = path.resolve(
43+
this.artifactInfo.rootDir,
44+
'models'
45+
);
46+
this.artifactInfo.repositoryDir = path.resolve(
47+
this.artifactInfo.rootDir,
48+
'repositories'
49+
);
50+
51+
this.option('controllerType', {
52+
type: String,
53+
required: false,
54+
description: 'Type for the ' + this.artifactInfo.type,
55+
});
56+
2657
return super._setupGenerator();
2758
}
2859

@@ -34,25 +65,154 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
3465
return super.promptArtifactName();
3566
}
3667

68+
promptArtifactType() {
69+
debug('Prompting for controller type');
70+
return this.prompt([
71+
{
72+
type: 'list',
73+
name: 'controllerType',
74+
message: 'What kind of controller would you like to generate?',
75+
when: this.artifactInfo.controllerType === undefined,
76+
choices: [ControllerGenerator.BASIC, ControllerGenerator.REST],
77+
default: ControllerGenerator.BASIC,
78+
},
79+
])
80+
.then(props => {
81+
Object.assign(this.artifactInfo, props);
82+
return props;
83+
})
84+
.catch(err => {
85+
debug(`Error during controller type prompt: ${err.stack}`);
86+
return this.exit(err);
87+
});
88+
}
89+
90+
promptArtifactCrudVars() {
91+
let modelList = [];
92+
let repositoryList = [];
93+
if (
94+
!this.artifactInfo.controllerType ||
95+
this.artifactInfo.controllerType === ControllerGenerator.BASIC
96+
) {
97+
return;
98+
}
99+
return utils
100+
.getArtifactList(this.artifactInfo.modelDir, 'model')
101+
.then(list => {
102+
if (_.isEmpty(list)) {
103+
return Promise.reject(
104+
new Error(`No models found in ${this.artifactInfo.modelDir}`)
105+
);
106+
}
107+
modelList = list;
108+
return utils.getArtifactList(
109+
this.artifactInfo.repositoryDir,
110+
'repository',
111+
true
112+
);
113+
})
114+
.then(list => {
115+
if (_.isEmpty(list)) {
116+
return Promise.reject(
117+
new Error(
118+
`No repositories found in ${this.artifactInfo.repositoryDir}`
119+
)
120+
);
121+
}
122+
repositoryList = list;
123+
return this.prompt([
124+
{
125+
type: 'list',
126+
name: 'modelName',
127+
message:
128+
'What is the name of the model to use with this CRUD repository?',
129+
choices: modelList,
130+
when: this.artifactInfo.modelName === undefined,
131+
default: modelList[0],
132+
validate: utils.validateClassName,
133+
},
134+
{
135+
type: 'list',
136+
name: 'repositoryName',
137+
message: 'What is the name of your CRUD repository?',
138+
choices: repositoryList,
139+
when: this.artifactInfo.repositoryName === undefined,
140+
default: repositoryList[0],
141+
validate: utils.validateClassName,
142+
},
143+
{
144+
type: 'list',
145+
name: 'idType',
146+
message: 'What is the type of your ID?',
147+
choices: ['number', 'string', 'object'],
148+
when: this.artifactInfo.idType === undefined,
149+
default: 'number',
150+
},
151+
]).then(props => {
152+
debug(`props: ${inspect(props)}`);
153+
Object.assign(this.artifactInfo, props);
154+
// Ensure that the artifact names are valid.
155+
[
156+
this.artifactInfo.name,
157+
this.artifactInfo.modelName,
158+
this.artifactInfo.repositoryName,
159+
].forEach(item => {
160+
item = utils.toClassName(item);
161+
});
162+
// Create camel-case names for variables.
163+
this.artifactInfo.repositoryNameCamel = utils.camelCase(
164+
this.artifactInfo.repositoryName
165+
);
166+
this.artifactInfo.modelNameCamel = utils.camelCase(
167+
this.artifactInfo.modelName
168+
);
169+
return props;
170+
});
171+
})
172+
.catch(err => {
173+
debug(`Error during prompt for controller variables: ${err}`);
174+
return this.exit(err);
175+
});
176+
}
177+
37178
scaffold() {
38-
super.scaffold();
179+
// We don't want to call the base scaffold function since it copies
180+
// all of the templates!
39181
if (this.shouldExit()) return false;
182+
this.artifactInfo.name = utils.toClassName(this.artifactInfo.name);
40183
this.artifactInfo.filename =
41184
utils.kebabCase(this.artifactInfo.name) + '.controller.ts';
42185
if (debug.enabled) {
43186
debug(`Artifact filename set to: ${this.artifactInfo.filename}`);
44187
}
45188
// renames the file
46-
const source = this.destinationPath(
47-
this.artifactInfo.outdir + 'controller-template.ts'
48-
);
189+
let template = 'controller-template.ts';
190+
switch (this.artifactInfo.controllerType) {
191+
case ControllerGenerator.REST:
192+
template = 'controller-rest-template.ts';
193+
break;
194+
default:
195+
break;
196+
}
197+
const source = this.templatePath(path.join('src', 'controllers', template));
198+
if (debug.enabled) {
199+
debug(`Using template at: ${source}`);
200+
}
49201
const dest = this.destinationPath(
50-
this.artifactInfo.outdir + this.artifactInfo.filename
202+
path.join(this.artifactInfo.outdir, this.artifactInfo.filename)
51203
);
204+
52205
if (debug.enabled) {
206+
debug(`artifactInfo: ${inspect(this.artifactInfo)}`);
53207
debug(`Copying artifact to: ${dest}`);
54208
}
55-
this.fs.move(source, dest, {globOptions: {dot: true}});
209+
this.fs.copyTpl(
210+
source,
211+
dest,
212+
this.artifactInfo,
213+
{},
214+
{globOptions: {dot: true}}
215+
);
56216
return;
57217
}
58218

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {Filter, Where} from '@loopback/repository';
2+
import {post, param, get, put, patch, del} from '@loopback/openapi-v2';
3+
import {inject} from '@loopback/context';
4+
import {<%= modelName %>} from '../models';
5+
import {<%= repositoryName %>} from '../repositories';
6+
7+
export class <%= name %>Controller {
8+
9+
constructor(
10+
@inject('repositories.<%= modelNameCamel %>')
11+
public <%= repositoryNameCamel %> : <%= repositoryName %>,
12+
) {}
13+
14+
@post('/<%= modelNameCamel %>')
15+
@param.body('obj', <%= modelName %>)
16+
async create(obj: <%= modelName %>) : Promise<<%= modelName %>> {
17+
return await this.<%= repositoryNameCamel %>.create(obj);
18+
}
19+
20+
@get('/<%= modelNameCamel %>/count')
21+
@param.query.string('where')
22+
async count(where: Where) : Promise<number> {
23+
return await this.<%= repositoryNameCamel %>.count(where);
24+
}
25+
26+
@get('/<%= modelNameCamel %>')
27+
@param.query.string('filter')
28+
async find(filter: Filter) : Promise<<%= modelName %>[]> {
29+
return await this.<%= repositoryNameCamel %>.find(filter);
30+
}
31+
32+
@patch('/<%= modelNameCamel %>')
33+
@param.query.string('where')
34+
@param.body('obj', <%= modelName %>)
35+
async updateAll(where: Where, obj: <%= modelName %>) : Promise<number> {
36+
return await this.<%= repositoryNameCamel %>.updateAll(where, obj);
37+
}
38+
39+
@del('/<%= modelNameCamel %>')
40+
@param.query.string('where')
41+
async deleteAll(where: Where) : Promise<number> {
42+
return await this.<%= repositoryNameCamel %>.deleteAll(where);
43+
}
44+
45+
@get('/<%= modelNameCamel %>/{id}')
46+
async findById(id: <%= idType %>) : Promise<<%= modelName %>> {
47+
return await this.<%= repositoryNameCamel %>.findById(id);
48+
}
49+
50+
@patch('/<%= modelNameCamel %>/{id}')
51+
@param.body('obj', <%= modelName %>)
52+
async updateById(id: <%= idType %>, obj: <%= modelName %>) : Promise<boolean> {
53+
return await this.<%= repositoryNameCamel %>.updateById(id, obj);
54+
}
55+
56+
@del('/<%= modelNameCamel %>/{id}')
57+
async deleteById(id: <%= idType %>) : Promise<boolean> {
58+
return await this.<%= repositoryNameCamel %>.deleteById(id);
59+
}
60+
}

packages/cli/lib/artifact-generator.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ module.exports = class ArtifactGenerator extends BaseGenerator {
8282

8383
return this.prompt(prompts).then(props => {
8484
Object.assign(this.artifactInfo, props);
85+
return props;
8586
});
8687
}
8788

packages/cli/lib/base-generator.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ module.exports = class BaseGenerator extends Generator {
2121
* Subclasses can extend _setupGenerator() to set up the generator
2222
*/
2323
_setupGenerator() {
24-
// No operation
24+
this.artifactInfo = {
25+
rootDir: 'src',
26+
};
2527
}
2628

2729
/**

0 commit comments

Comments
 (0)