Skip to content

Commit 3593820

Browse files
committed
feat(cli): lb4 model command to scaffold model files
1 parent 1e92f4f commit 3593820

File tree

8 files changed

+416
-3
lines changed

8 files changed

+416
-3
lines changed

packages/cli/bin/cli.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function setupGenerators() {
6565
path.join(__dirname, '../generators/datasource'),
6666
PREFIX + 'datasource',
6767
);
68+
env.register(path.join(__dirname, '../generators/model'), PREFIX + 'model');
6869
env.register(
6970
path.join(__dirname, '../generators/example'),
7071
PREFIX + 'example',

packages/cli/generators/controller/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
197197
if (debug.enabled) {
198198
debug(`Artifact output filename set to: ${this.artifactInfo.outFile}`);
199199
}
200+
200201
// renames the file
201202
let template = 'controller-template.ts.ejs';
202203
switch (this.artifactInfo.controllerType) {
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
2+
// Node module: @loopback/cli
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
'use strict';
7+
8+
const ArtifactGenerator = require('../../lib/artifact-generator');
9+
const debug = require('../../lib/debug')('model-generator');
10+
const utils = require('../../lib/utils');
11+
const chalk = require('chalk');
12+
const path = require('path');
13+
14+
/**
15+
* Model Generator
16+
*
17+
* Prompts for a Model name, Model Base Class (currently defaults to 'Entity').
18+
* Creates the Model Class -- currently a one time process only.
19+
*
20+
* Will prompt for properties to add to the Model till a blank property name is
21+
* entered. Will also ask if a property is required, the default value for the
22+
* property, if it's the ID (unless one has been selected), etc.
23+
*/
24+
module.exports = class ModelGenerator extends ArtifactGenerator {
25+
constructor(args, opts) {
26+
super(args, opts);
27+
}
28+
29+
_setupGenerator() {
30+
this.artifactInfo = {
31+
type: 'model',
32+
rootDir: 'src',
33+
};
34+
35+
this.artifactInfo.outDir = path.resolve(
36+
this.artifactInfo.rootDir,
37+
'models',
38+
);
39+
40+
// Model Property Types
41+
this.typeChoices = [
42+
'string',
43+
'number',
44+
'boolean',
45+
'object',
46+
'array',
47+
'date',
48+
'buffer',
49+
'geopoint',
50+
'any',
51+
];
52+
53+
this.artifactInfo.properties = {};
54+
this.propCounter = 0;
55+
56+
return super._setupGenerator();
57+
}
58+
59+
setOptions() {
60+
return super.setOptions();
61+
}
62+
63+
checkLoopBackProject() {
64+
return super.checkLoopBackProject();
65+
}
66+
67+
async promptArtifactName() {
68+
await super.promptArtifactName();
69+
this.artifactInfo.className = utils.toClassName(this.artifactInfo.name);
70+
this.log(
71+
`Let's add a property to ${chalk.yellow(this.artifactInfo.className)}`,
72+
);
73+
}
74+
75+
// Prompt for a property name
76+
async promptPropertyName() {
77+
this.log(`Enter an empty property name when done`);
78+
79+
delete this.propName;
80+
81+
const prompts = [
82+
{
83+
name: 'propName',
84+
message: 'Enter the property name:',
85+
validate: function(val) {
86+
if (val) {
87+
return utils.checkPropertyName(val);
88+
} else {
89+
return true;
90+
}
91+
},
92+
},
93+
];
94+
95+
const answers = await this.prompt(prompts);
96+
debug(`propName => ${JSON.stringify(answers)}`);
97+
if (answers.propName) {
98+
this.artifactInfo.properties[answers.propName] = {};
99+
this.propName = answers.propName;
100+
}
101+
return this._promptPropertyInfo();
102+
}
103+
104+
// Internal Method. Called when a new property is entered.
105+
// Prompts the user for more information about the property to be added.
106+
async _promptPropertyInfo() {
107+
if (!this.propName) {
108+
return true;
109+
} else {
110+
const prompts = [
111+
{
112+
name: 'type',
113+
message: 'Property type:',
114+
type: 'list',
115+
choices: this.typeChoices,
116+
},
117+
{
118+
name: 'arrayType',
119+
message: 'Type of array items:',
120+
type: 'list',
121+
choices: this.typeChoices.filter(choice => {
122+
return choice !== 'array';
123+
}),
124+
when: answers => {
125+
return answers.type === 'array';
126+
},
127+
},
128+
{
129+
name: 'id',
130+
message: 'Is ID field?',
131+
type: 'confirm',
132+
default: false,
133+
when: answers => {
134+
return (
135+
!this.idFieldSet &&
136+
!['array', 'object', 'buffer'].includes(answers.type)
137+
);
138+
},
139+
},
140+
{
141+
name: 'required',
142+
message: 'Required?:',
143+
type: 'confirm',
144+
default: false,
145+
},
146+
{
147+
name: 'default',
148+
message: `Default value ${chalk.yellow('[leave blank for none]')}:`,
149+
when: answers => {
150+
return ![null, 'buffer', 'any'].includes(answers.type);
151+
},
152+
},
153+
];
154+
155+
const answers = await this.prompt(prompts);
156+
debug(`propertyInfo => ${JSON.stringify(answers)}`);
157+
if (answers.default === '') {
158+
delete answers.default;
159+
}
160+
161+
Object.assign(this.artifactInfo.properties[this.propName], answers);
162+
if (answers.id) {
163+
this.idFieldSet = true;
164+
}
165+
166+
this.log(
167+
`Let's add another property to ${chalk.yellow(
168+
this.artifactInfo.className,
169+
)}`,
170+
);
171+
return this.promptPropertyName();
172+
}
173+
}
174+
175+
scaffold() {
176+
if (this.shouldExit()) return false;
177+
178+
debug('scaffolding');
179+
180+
// Data for templates
181+
this.artifactInfo.fileName = utils.kebabCase(this.artifactInfo.name);
182+
this.artifactInfo.outFile = `${this.artifactInfo.fileName}.model.ts`;
183+
184+
// Resolved Output Path
185+
const tsPath = this.destinationPath(
186+
this.artifactInfo.outDir,
187+
this.artifactInfo.outFile,
188+
);
189+
190+
const modelTemplatePath = this.templatePath('model.ts.ejs');
191+
192+
// Set up types for Templating
193+
const TS_TYPES = ['string', 'number', 'object', 'boolean', 'any'];
194+
const NON_TS_TYPES = ['geopoint', 'date'];
195+
Object.entries(this.artifactInfo.properties).forEach(([key, val]) => {
196+
// Default tsType is the type property
197+
val.tsType = val.type;
198+
199+
// Override tsType based on certain type values
200+
if (val.type === 'array') {
201+
if (TS_TYPES.includes(val.arrayType)) {
202+
val.tsType = `${val.arrayType}[]`;
203+
} else if (val.type === 'buffer') {
204+
val.tsType = `Buffer[]`;
205+
} else {
206+
val.tsType = `string[]`;
207+
}
208+
} else if (val.type === 'buffer') {
209+
val.tsType = 'Buffer';
210+
}
211+
212+
if (NON_TS_TYPES.includes(val.tsType)) {
213+
val.tsType = 'string';
214+
}
215+
216+
if (
217+
val.defaultValue &&
218+
NON_TS_TYPES.concat(['string', 'any']).includes(val.type)
219+
) {
220+
val.defaultValue = `'${val.defaultValue}'`;
221+
}
222+
223+
// Convert Type to include '' for template
224+
val.type = `'${val.type}'`;
225+
226+
if (!val.required) {
227+
delete val.required;
228+
}
229+
230+
if (!val.id) {
231+
delete val.id;
232+
}
233+
});
234+
235+
this.fs.copyTpl(modelTemplatePath, tsPath, this.artifactInfo);
236+
}
237+
238+
async end() {
239+
await super.end();
240+
}
241+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Entity, model, property} from '@loopback/repository';
2+
3+
@model()
4+
export class <%= className %> extends Entity {
5+
<% Object.entries(properties).forEach(([key, val]) => { %>
6+
@property({<% Object.entries(val).forEach(([propKey, propVal]) => {%>
7+
<%if (propKey !== 'tsType') {%><%= propKey %>: <%- propVal %>,<% } %><% }) %>
8+
})
9+
<%= key %><%if (!val.required) {%>?<% } %>: <%= val.tsType %>;
10+
<% }) %>
11+
constructor(data?: Partial<<%= className %>>) {
12+
super(data);
13+
}
14+
}

packages/cli/lib/utils.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const Conflicter = require('yeoman-generator/lib/util/conflicter');
2525

2626
const readdirAsync = promisify(fs.readdir);
2727

28+
const RESERVED_PROPERTY_NAMES = ['constructor'];
29+
2830
/**
2931
* Either a reference to util.promisify or its polyfill, depending on
3032
* your version of Node.
@@ -328,3 +330,43 @@ exports.readTextFromStdin = function() {
328330
});
329331
});
330332
};
333+
334+
/*
335+
* Validate property name
336+
* @param {String} name The user input
337+
* @returns {String|Boolean}
338+
*/
339+
exports.checkPropertyName = function(name) {
340+
var result = exports.validateRequiredName(name);
341+
if (result !== true) return result;
342+
if (RESERVED_PROPERTY_NAMES.includes(name)) {
343+
return `${name} is a reserved keyword. Please use another name`;
344+
}
345+
return true;
346+
};
347+
348+
/**
349+
* Validate required name for properties, data sources, or connectors
350+
* Empty name could not pass
351+
* @param {String} name The user input
352+
* @returns {String|Boolean}
353+
*/
354+
exports.validateRequiredName = function(name) {
355+
if (!name) {
356+
return 'Name is required';
357+
}
358+
return validateValue(name, /[\/@\s\+%:\.]/);
359+
};
360+
361+
function validateValue(name, unallowedCharacters) {
362+
if (!unallowedCharacters) {
363+
unallowedCharacters = /[\/@\s\+%:\.]/;
364+
}
365+
if (name.match(unallowedCharacters)) {
366+
return `Name cannot contain special characters ${unallowedCharacters}: ${name}`;
367+
}
368+
if (name !== encodeURIComponent(name)) {
369+
return `Name cannot contain special characters escaped by encodeURIComponent: ${name}`;
370+
}
371+
return true;
372+
}

packages/cli/test/integration/cli/cli.integration.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('cli', () => {
2424
expect(entries).to.eql([
2525
'Available commands: ',
2626
' lb4 app\n lb4 extension\n lb4 controller\n lb4 datasource\n ' +
27-
'lb4 example\n lb4 openapi',
27+
'lb4 model\n lb4 example\n lb4 openapi',
2828
]);
2929
});
3030

@@ -42,7 +42,7 @@ describe('cli', () => {
4242
expect(entries).to.containEql('Available commands: ');
4343
expect(entries).to.containEql(
4444
' lb4 app\n lb4 extension\n lb4 controller\n lb4 datasource\n ' +
45-
'lb4 example\n lb4 openapi',
45+
'lb4 model\n lb4 example\n lb4 openapi',
4646
);
4747
});
4848

0 commit comments

Comments
 (0)