-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
feat(cli): lb4 model
to scaffold model files
#1487
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 |
---|---|---|
|
@@ -3,7 +3,7 @@ | |
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {Entity, property, model} from '@loopback/repository'; | ||
import {Entity, model, property} from '@loopback/repository'; | ||
|
||
@model() | ||
export class Todo extends Entity { | ||
|
@@ -27,22 +27,18 @@ export class Todo extends Entity { | |
@property({ | ||
type: 'boolean', | ||
}) | ||
isComplete: boolean; | ||
isComplete?: boolean; | ||
|
||
@property({ | ||
type: 'string', | ||
}) | ||
remindAtAddress: string; // address,city,zipcode | ||
remindAtAddress?: string; // address,city,zipcode | ||
|
||
// TODO(bajtos) Use LoopBack's GeoPoint type here | ||
@property({ | ||
type: 'string', | ||
}) | ||
remindAtGeo: string; // latitude,longitude | ||
|
||
getId() { | ||
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. What happened to this function - are you sure we don't need it any more? 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. Should we perhaps modify the template used by the CLI tool to always include 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 think with the refactor my comment on GitHub got lost somewhere ... but the reason for me to not include the
Here's the reference code: https://github.com/strongloop/loopback-next/blob/08c2d896367a7f8ecc5153578f2a698eefdea7a1/packages/repository/src/model.ts#L218-L231 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. Good point. I guess my concern is about performance - an explicit But as Donald Knuth said, premature optimization is the root of all evil. I am ok with your proposed change, we can always add explicit |
||
return this.id; | ||
} | ||
remindAtGeo?: string; // latitude,longitude | ||
|
||
constructor(data?: Partial<Todo>) { | ||
super(data); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
// Copyright IBM Corp. 2017,2018. 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 ArtifactGenerator = require('../../lib/artifact-generator'); | ||
const debug = require('../../lib/debug')('model-generator'); | ||
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. ditto with 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. Good catch. Added 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. oops :-), never mind in that case. |
||
const utils = require('../../lib/utils'); | ||
const chalk = require('chalk'); | ||
const path = require('path'); | ||
|
||
/** | ||
* Model Generator | ||
* | ||
* Prompts for a Model name, Model Base Class (currently defaults to 'Entity'). | ||
* Creates the Model Class -- currently a one time process only. | ||
* | ||
* Will prompt for properties to add to the Model till a blank property name is | ||
* entered. Will also ask if a property is required, the default value for the | ||
* property, if it's the ID (unless one has been selected), etc. | ||
*/ | ||
module.exports = class ModelGenerator extends ArtifactGenerator { | ||
constructor(args, opts) { | ||
super(args, opts); | ||
} | ||
|
||
_setupGenerator() { | ||
this.artifactInfo = { | ||
type: 'model', | ||
rootDir: 'src', | ||
}; | ||
|
||
this.artifactInfo.outDir = path.resolve( | ||
this.artifactInfo.rootDir, | ||
'models', | ||
); | ||
|
||
// Model Property Types | ||
this.typeChoices = [ | ||
'string', | ||
'number', | ||
'boolean', | ||
'object', | ||
'array', | ||
'date', | ||
'buffer', | ||
'geopoint', | ||
'any', | ||
]; | ||
|
||
this.artifactInfo.properties = {}; | ||
this.propCounter = 0; | ||
|
||
return super._setupGenerator(); | ||
} | ||
|
||
setOptions() { | ||
return super.setOptions(); | ||
} | ||
|
||
checkLoopBackProject() { | ||
return super.checkLoopBackProject(); | ||
} | ||
|
||
async promptArtifactName() { | ||
await super.promptArtifactName(); | ||
this.artifactInfo.className = utils.toClassName(this.artifactInfo.name); | ||
this.log( | ||
`Let's add a property to ${chalk.yellow(this.artifactInfo.className)}`, | ||
); | ||
} | ||
|
||
// Prompt for a property name | ||
async promptPropertyName() { | ||
this.log(`Enter an empty property name when done`); | ||
|
||
delete this.propName; | ||
|
||
const prompts = [ | ||
{ | ||
name: 'propName', | ||
message: 'Enter the property name:', | ||
validate: function(val) { | ||
if (val) { | ||
return utils.checkPropertyName(val); | ||
} else { | ||
return true; | ||
} | ||
}, | ||
}, | ||
]; | ||
|
||
const answers = await this.prompt(prompts); | ||
debug(`propName => ${JSON.stringify(answers)}`); | ||
if (answers.propName) { | ||
this.artifactInfo.properties[answers.propName] = {}; | ||
this.propName = answers.propName; | ||
} | ||
return this._promptPropertyInfo(); | ||
} | ||
|
||
// Internal Method. Called when a new property is entered. | ||
// Prompts the user for more information about the property to be added. | ||
async _promptPropertyInfo() { | ||
if (!this.propName) { | ||
return true; | ||
} else { | ||
const prompts = [ | ||
{ | ||
name: 'type', | ||
message: 'Property type:', | ||
type: 'list', | ||
choices: this.typeChoices, | ||
}, | ||
{ | ||
name: 'arrayType', | ||
message: 'Type of array items:', | ||
type: 'list', | ||
choices: this.typeChoices.filter(choice => { | ||
return choice !== 'array'; | ||
}), | ||
when: answers => { | ||
return answers.type === 'array'; | ||
}, | ||
}, | ||
{ | ||
name: 'id', | ||
message: 'Is ID field?', | ||
type: 'confirm', | ||
default: false, | ||
when: answers => { | ||
return ( | ||
!this.idFieldSet && | ||
!['array', 'object', 'buffer'].includes(answers.type) | ||
); | ||
}, | ||
}, | ||
{ | ||
name: 'required', | ||
message: 'Required?:', | ||
type: 'confirm', | ||
default: false, | ||
}, | ||
{ | ||
name: 'default', | ||
message: `Default value ${chalk.yellow('[leave blank for none]')}:`, | ||
when: answers => { | ||
return ![null, 'buffer', 'any'].includes(answers.type); | ||
}, | ||
}, | ||
]; | ||
|
||
const answers = await this.prompt(prompts); | ||
debug(`propertyInfo => ${JSON.stringify(answers)}`); | ||
if (answers.default === '') { | ||
delete answers.default; | ||
} | ||
|
||
Object.assign(this.artifactInfo.properties[this.propName], answers); | ||
if (answers.id) { | ||
this.idFieldSet = true; | ||
} | ||
|
||
this.log( | ||
`Let's add another property to ${chalk.yellow( | ||
this.artifactInfo.className, | ||
)}`, | ||
); | ||
return this.promptPropertyName(); | ||
} | ||
} | ||
|
||
scaffold() { | ||
if (this.shouldExit()) return false; | ||
|
||
debug('scaffolding'); | ||
|
||
// Data for templates | ||
this.artifactInfo.fileName = utils.kebabCase(this.artifactInfo.name); | ||
this.artifactInfo.outFile = `${this.artifactInfo.fileName}.model.ts`; | ||
|
||
// Resolved Output Path | ||
const tsPath = this.destinationPath( | ||
this.artifactInfo.outDir, | ||
this.artifactInfo.outFile, | ||
); | ||
|
||
const modelTemplatePath = this.templatePath('model.ts.ejs'); | ||
|
||
// Set up types for Templating | ||
const TS_TYPES = ['string', 'number', 'object', 'boolean', 'any']; | ||
const NON_TS_TYPES = ['geopoint', 'date']; | ||
Object.entries(this.artifactInfo.properties).forEach(([key, val]) => { | ||
// Default tsType is the type property | ||
val.tsType = val.type; | ||
|
||
// Override tsType based on certain type values | ||
if (val.type === 'array') { | ||
if (TS_TYPES.includes(val.arrayType)) { | ||
val.tsType = `${val.arrayType}[]`; | ||
} else if (val.type === 'buffer') { | ||
val.tsType = `Buffer[]`; | ||
} else { | ||
val.tsType = `string[]`; | ||
} | ||
} else if (val.type === 'buffer') { | ||
val.tsType = 'Buffer'; | ||
} | ||
|
||
if (NON_TS_TYPES.includes(val.tsType)) { | ||
val.tsType = 'string'; | ||
} | ||
|
||
if ( | ||
val.defaultValue && | ||
NON_TS_TYPES.concat(['string', 'any']).includes(val.type) | ||
) { | ||
val.defaultValue = `'${val.defaultValue}'`; | ||
} | ||
|
||
// Convert Type to include '' for template | ||
val.type = `'${val.type}'`; | ||
|
||
if (!val.required) { | ||
delete val.required; | ||
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? What I'm getting from this is if 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. The code is actually designed to delete |
||
} | ||
|
||
if (!val.id) { | ||
delete val.id; | ||
} | ||
}); | ||
|
||
this.fs.copyTpl(modelTemplatePath, tsPath, this.artifactInfo); | ||
} | ||
|
||
async end() { | ||
await super.end(); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {Entity, model, property} from '@loopback/repository'; | ||
|
||
@model() | ||
export class <%= className %> extends Entity { | ||
<% Object.entries(properties).forEach(([key, val]) => { %> | ||
@property({<% Object.entries(val).forEach(([propKey, propVal]) => {%> | ||
<%if (propKey !== 'tsType') {%><%= propKey %>: <%- propVal %>,<% } %><% }) %> | ||
}) | ||
<%= key %><%if (!val.required) {%>?<% } %>: <%= val.tsType %>; | ||
<% }) %> | ||
constructor(data?: Partial<<%= className %>>) { | ||
super(data); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -93,27 +93,7 @@ module.exports = class ArtifactGenerator extends BaseGenerator { | |
}); | ||
|
||
if (generationStatus) { | ||
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. Ideally, I think we should models/model-1.ts (success) 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. Would it be ok if I made a follow up task for this -- I assume this is needed for |
||
/** | ||
* Update the index.ts in this.artifactInfo.outDir. Creates it if it | ||
* doesn't exist. | ||
* this.artifactInfo.outFile is what is exported from the file. | ||
* | ||
* Both those properties must be present for this to happen. Optionally, | ||
* this can be disabled even if the properties are present by setting: | ||
* this.artifactInfo.disableIndexUpdate = true; | ||
*/ | ||
if ( | ||
this.artifactInfo.outDir && | ||
this.artifactInfo.outFile && | ||
!this.artifactInfo.disableIndexUpdate | ||
) { | ||
await updateIndex(this.artifactInfo.outDir, this.artifactInfo.outFile); | ||
// Output for users | ||
this.log( | ||
chalk.green(' update'), | ||
`${this.artifactInfo.relPath}/index.ts`, | ||
); | ||
} | ||
await this._updateIndexFiles(); | ||
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. Should 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. It's part of 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. 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. Moving it would mean also moving/duplication the |
||
|
||
// User Output | ||
this.log(); | ||
|
@@ -128,4 +108,55 @@ module.exports = class ArtifactGenerator extends BaseGenerator { | |
|
||
await super.end(); | ||
} | ||
|
||
/** | ||
* Update the index.ts in this.artifactInfo.outDir. Creates it if it | ||
* doesn't exist. | ||
* this.artifactInfo.outFile is what is exported from the file. | ||
* | ||
* Both those properties must be present for this to happen. Optionally, | ||
* this can be disabled even if the properties are present by setting: | ||
* this.artifactInfo.disableIndexUpdate = true; | ||
* | ||
* Multiple indexes / files can be updated by providing an array of | ||
* index update objects as follows: | ||
* this.artifactInfo.indexesToBeUpdated = [{ | ||
* dir: 'directory in which to update index.ts', | ||
* file: 'file to add to index.ts', | ||
* }, {dir: '...', file: '...'}] | ||
*/ | ||
async _updateIndexFiles() { | ||
// Index Update Disabled | ||
if (this.artifactInfo.disableIndexUpdate) return; | ||
|
||
// No Array given for Index Update, Create default array | ||
if ( | ||
!this.artifactInfo.indexesToBeUpdated && | ||
this.artifactInfo.outDir && | ||
this.artifactInfo.outFile | ||
) { | ||
this.artifactInfo.indexesToBeUpdated = [ | ||
{dir: this.artifactInfo.outDir, file: this.artifactInfo.outFile}, | ||
]; | ||
} else { | ||
this.artifactInfo.indexesToBeUpdated = []; | ||
} | ||
|
||
for (const idx of this.artifactInfo.indexesToBeUpdated) { | ||
await updateIndex(idx.dir, idx.file); | ||
// Output for users | ||
const updateDirRelPath = path.relative( | ||
this.artifactInfo.relPath, | ||
idx.dir, | ||
); | ||
|
||
const outPath = path.join( | ||
this.artifactInfo.relPath, | ||
updateDirRelPath, | ||
'index.ts', | ||
); | ||
|
||
this.log(chalk.green(' update'), `${outPath}`); | ||
} | ||
} | ||
}; |
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.
Any reason to change this to optional? If we do change it, could you also find references to this model in the documentation and fix that up as well?
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.
FWIW, the decoration
@property({type: 'boolean'})
means that the property is optional at least as far as the persistence layer is concerned (juggler & connectors).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.
Only reason is this is how the CLI generated file will look ... I wanted to keep it consistent for users looking at this vs. what they get when they follow the tutorial.
Docs (including tutorial) will be updated in a follow up PR.