-
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
Conversation
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.
Looks 🤩.
I think in order to test the property section correctly, it's necessary to have a property generator.
For the sake of avoiding scope creep, we should do the bare minimum to make the property generator work solely with the lb4 model
command.
I do remember you talking about there being a lot of work to make a feature complete property generator. Could you consider which features can be left out to make just the model
command functional?
chalk.green(' update'), | ||
`${this.artifactInfo.relPath}/index.ts`, | ||
); | ||
for (let i = 0; i < this.artifactInfo.updateIndexes.length; i++) { |
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 not use for ... of
loop here?
type: 'list', | ||
default: baseOptions[0], | ||
choices: baseOptions, | ||
when: baseOptions.length > 1, |
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.
Will this run? baseOptions
is only populated with one element
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.
It's not intended to run ATM but designed to run if / when we add more baseOptions
in the future.
when: function(answers) { | ||
return ( | ||
![null, 'buffer', 'any'].includes(answers.type) && | ||
this.typeChoices.includes(answers.type) |
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.
Is the second condition here necessary?
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.
Oops, A remnant from the lb3
CLI which allows for custom types to be entered ... something we'll probably add later on.
|
||
// Set up types for Templating | ||
const TS_TYPES = ['string', 'number', 'object', 'boolean', 'any']; | ||
const NON_TS_TYPES = ['geopoint', 'date']; |
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.
I thought date type would be where we'd use JavaScript's own Date
like Buffer.
I think we should have something like const COMPLEX_TS_TYPES = ['buffer', 'date']
and then have a matching series of if statements to go with it
const TS_TYPES = ['string', 'number', 'object', 'boolean', 'any']; | ||
const NON_TS_TYPES = ['geopoint', 'date']; | ||
Object.entries(this.artifactInfo.properties).forEach(([key, val]) => { | ||
val.tsType = val.type; |
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.
Can we have this statement way down at the bottom?
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 move it? Being at the top it acts as the default value without requiring a check to see if val.tsType
exists or not.
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.
I was also a bit confused when looking at this, but that makes sense 👍
@@ -23,6 +23,8 @@ const Conflicter = require('yeoman-generator/lib/util/conflicter'); | |||
|
|||
const readdirAsync = promisify(fs.readdir); | |||
|
|||
const RESERVED_PROPERTY_NAMES = ['constructor']; |
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.
Let's also include access level keywords (like public) and any other applicable keywords from this list
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.
public
is actually allowed as a property on a class ... surprising but TS didn't complain when I tried it.
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.
🤔
packages/cli/lib/utils.js
Outdated
unallowedCharacters = /[\/@\s\+%:\.]/; | ||
} | ||
if (name.match(unallowedCharacters)) { | ||
return `Name cannot contain special characters ${unallowedCharacters} ${name}`; |
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.
missing a colon between the two variables
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.
Also, consider using parts of this: d6f1a64#diff-690866a08191a550353b1a1929fc6080R38
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.
Great feature!! LGTM in general, left a few comments.
"id": { | ||
"type": "number", | ||
"id": true, | ||
"required": false |
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.
nitpick feel free to ignore: I think we can omit the required
field when it's false
.
const modelDef = require('../todo.model.json'); | ||
|
||
export class TodoBase extends Entity { | ||
static definition = new ModelDefinition(modelDef); |
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.
Do we need to keep getId()
function? see the old todo.model.ts
file
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.
We actually provide a getId()
function in the Entity
class that can check for a property with id: true
and returns that. I'm actually not sure why we had an override. See: https://github.com/strongloop/loopback-next/blob/master/packages/repository/src/model.ts#L193-L212
cc: @bajtos Any ideas as to why this is overridden? Maybe I'm missing something?
type: 'list', | ||
default: baseOptions[0], | ||
choices: baseOptions, | ||
when: baseOptions.length > 1, |
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.
hmm, if the baseOptions
is hard-coded on Line71, how would this prompt be triggered?
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.
It's not intended to run ATM but designed to run if / when we add more baseOptions
in the future.
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.
I usually prefer to leave out code that will be used only later in the future because of YAGNI, but don't mind too much to have it here.
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.
Took it out -- it's simple enough logic that it can be added without pain when needed.
|
||
Object.assign(this.artifactInfo.properties[this.propName], props); | ||
if (props.id) { | ||
this.idFieldSet = true; |
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.
I love the check here to prevent user from answering the question everytime when add a new property.
But not sure would it make sense for composed id, see https://loopback.io/doc/en/lb3/Model-definition-JSON-file.html#id-properties
IMO composed id is out of the scope of this PR though.
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.
I'll create a follow up issue to discuss composite IDs and how we will support them.
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.
Before we focus on implementation details, let's discuss the changes at high-level please. I'll post a longer comment in a moment.
This pull request changes the way how models are defined by app developers. So far, we were using TypeScript decorators only. The proposal here is to switch to JSON-based configuration. I am personally not happy with that direction, as it forces developers to maintain model definitions in two places (TypeScript source containing TS API, a JSON file containing LB definition). I read through #1221 and did not find any mention of this change. In the opposite, the issue explicitly says:
I would like the generator to emit files and source code that's following the current approach as shown in
Thoughts? |
Game Plan Talked to both @bajtos and @raymondfeng to discuss the concerns and what we want. Here's the plan to land this feature (incremental, iterative approach).
Out of Scope of this PR:
|
UPDATE: Issue for discussing declarative support for models: #1491 |
lb4 model
to scaffold model fileslb4 model
to scaffold model files
lb4 model
to scaffold model fileslb4 model
to scaffold model files
The two The files will be removed before merging.
|
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.
Have you considered restructuring the way property prompts are run to accommodate for the tests? Since there's no way to recursively call the property prompt, maybe we can have our code internally support this use case:
const prompt = {
name: 'test',
properties: {
prop1: {propName: 'foo'},
prop2: {propName: 'bar'},
}
}
await testUtils.prompt(prompt)
And have the code look something like this:
async promptPropertyPrompts {
// somehow prompt for a false prompt that only sets up the `properties` key
// and at the same time prevent the logger from showing this process to the user
// this prompt would somehow populate the `properties` key set up earlier
this._promptProperty();
}
Admittedly, this is a very hack way of doing things. An alternative that we could consider is to do what we did here https://github.com/strongloop/loopback-next/blob/master/packages/cli/test/integration/lib/artifact-generator.js#L108
It might just be easier to just make a property generator 😂
EDIT: some review comments are hidden since I reviewed through each commits. Aside from the insignificant comment on async/await stuff, they're still valid though
@@ -27,22 +27,18 @@ export class Todo extends Entity { | |||
@property({ | |||
type: 'boolean', | |||
}) | |||
isComplete: boolean; | |||
isComplete?: boolean; |
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.
return this.prompt(prompts).then(props => { | ||
if (!props.base) props.base = baseOptions[0]; | ||
Object.assign(this.artifactInfo, props); | ||
const answers = await this.prompt(prompts); |
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.
Never knew this would work; I thought this.prompt()
never actually returned a promise. TIL'ed
val.type = `'${val.type}'`; | ||
|
||
if (!val.required) { | ||
delete val.required; |
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.
Is this necessary? What I'm getting from this is if val.required
does not exist, delete the key off of val
. Is the code manually defining the key as undefined
or null
somewhere?
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.
The code is actually designed to delete val.required
if required
is set to false
based on feedback from @jannyHou ... we don't need to decorate the property with required: false
since that's the default if not present.
await updateIndex(update.dir, update.file); | ||
if (!this.artifactInfo.disableIndexUpdate) { | ||
if ( | ||
!this.artifactInfo.updateIndexes && |
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.
We should change the name updateIndexes
to something like indexesToBeUpdated
since it could be functionally confusing with disableIndexUpdate
@@ -27,22 +27,18 @@ export class Todo extends Entity { | |||
@property({ | |||
type: 'boolean', | |||
}) | |||
isComplete: boolean; | |||
isComplete?: boolean; |
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).
|
||
// 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 comment
The 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 comment
The 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 getId()
implementation, preferably after the constructor
function?
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.
I think with the refactor my comment on GitHub got lost somewhere ... but the reason for me to not include the getId
function is as follows:
- The underlying
Entity
class actually implements it based on the metadata ... so any property can be theid
field and it handles it correctly (and while we can do this with the CLI ... I don't see the point of overriding the logic from the base class.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I guess my concern is about performance - an explicit getId
implementation returning this.id
is surely faster than the generic solution processing model definition.
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 getId()
implementation later, when there is a legitimate need.
type: 'list', | ||
default: baseOptions[0], | ||
choices: baseOptions, | ||
when: baseOptions.length > 1, |
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.
I usually prefer to leave out code that will be used only later in the future because of YAGNI, but don't mind too much to have it here.
I think this is sort of a pain point known for long time, see [CLI] Get rid of Yeoman #844:
|
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.
LGTM 👍
Please don't forget to remove the two .bak
files before landing.
chalk.green(' update'), | ||
`${this.artifactInfo.relPath}/index.ts`, | ||
); | ||
if (!this.artifactInfo.disableIndexUpdate) { |
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.
Could you please use reverse-logic and early return to reduce the amount of indentation and make the code easier to reason about?
if (this.artifactInfo.disableIndexUpdate) return;
I think we should move the code handling index updates into a new method, the end
method is already too long.
class ArtifactGenerator extends BaseGenerator {
// ...
async end() {
// ...
if (generationStatus) {
this._updateIndexFiles();
}
// User Output
this.log();
// ...
}
async _updateIndexFiles() {
if (this.artifactInfo.disableIndexUpdate) return;
// ...
}
}
Thoughts?
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.
On top of this, I have a question rather than a suggestion. Is this new logic covered by the following test in model.integration.js
?
describe('generator-loopback4:model', tests);
|
||
this.log( | ||
chalk.green(' update'), | ||
`${this.artifactInfo.relPath}/${updateDirRelPath}${ |
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.
I find multi-line templates difficult to read. Have you considered extracting individual bits into local variables?
const relPath = this.artifactInfo.relPath;
const sep = updateDirRelPath.length > 0 ? '/' : '';
const indexPath = `${relPath}/${updateDirRelPath}${sep}index.ts`;
this.log(chalk.green(' update'), indexPath);
I think it would be even better to use Node.js core API path.join(). Possible caveat: it may use \
instead of /
on Windows (don't remember what's the exact behavior), IDK if that would be a good or a bad thing.
const indexPath = path.join(
this.artifactInfo.relPath,
updateDirRelPath,
'index.ts'
);
this.log(chalk.green(' update'), indexPath);
The function handles empty path segments well:
$ node
> path.join('/asd', '', 'index.ts')
'/asd/index.ts'
const expect = testlab.expect; | ||
const TestSandbox = testlab.TestSandbox; | ||
|
||
// const ModelGenerator = require('../../../generators/model'); |
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.
Can we remove this commented line? (IMO we might want to rename the variable generator
below to modelGenerator
too while you're at it)
chalk.green(' update'), | ||
`${this.artifactInfo.relPath}/index.ts`, | ||
); | ||
if (!this.artifactInfo.disableIndexUpdate) { |
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.
On top of this, I have a question rather than a suggestion. Is this new logic covered by the following test in model.integration.js
?
describe('generator-loopback4:model', tests);
const utils = require('../../lib/utils'); | ||
const chalk = require('chalk'); | ||
const path = require('path'); | ||
const _ = require('lodash'); |
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.
I think lodash
is an unused import
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.
Good catch!
'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 comment
The reason will be displayed to describe this comment to others. Learn more.
ditto with debug
, it's not used in this file; consider using it to log the answers
we get back from the prompts perhaps or removing it altogether (none of the other generators have it). Also, same applies to the datasource generator index.js file.
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.
Good catch. Added debug
for property names and property info. debug
is used in the datasource generator ...
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.
oops :-), never mind in that case.
const TS_TYPES = ['string', 'number', 'object', 'boolean', 'any']; | ||
const NON_TS_TYPES = ['geopoint', 'date']; | ||
Object.entries(this.artifactInfo.properties).forEach(([key, val]) => { | ||
val.tsType = val.type; |
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.
I was also a bit confused when looking at this, but that makes sense 👍
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.
Most of my comments are nitpicks, consider applying them before landing (and removing the two .bak
files), but other than that LGTM.
This comment got collapsed:
No, the new logic for If you have any suggestions for this please let me know and I'll add the tests in a follow up PR. |
`${this.artifactInfo.relPath}/index.ts`, | ||
); | ||
} | ||
await this._updateIndexFiles(); |
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.
Should updateIndexFiles
be a step before end
?
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.
It's part of end
at the moment.
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.
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.
Moving it would mean also moving/duplication the generationStatus
check.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, I think we should generationStatus
file by file now instead of a global flag. For example,
models/model-1.ts (success)
models/model-2.ts (skipped)
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 it be ok if I made a follow up task for this -- I assume this is needed for lb4 openapi
which can call multiple generators under the hood?
I don't know enough of this package to suggest approaches but from what you've described, it makes sense why we didn't include tests for it. |
implements #1221
Commit Summary:
index.ts
fileslb4 model
commandexample-todo
to match output fromlb4 model
commandChecklist
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated