-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(cli): generate controller with CRUD methods #849
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
Conversation
4c633c5 to
44eed9f
Compare
|
The tests are currently failing in Node 6 due to lack of the |
shimks
left a comment
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/test/test-utils.js
Outdated
| * structure. | ||
| * @param {string} tmpDir Path to the temporary directory to setup | ||
| */ | ||
| exports.setupTmpDir = function(tmpDir) { |
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.
This function should be from @loopback/workspace in the future to create a dummy project.
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.
Please use a more descriptive name. setupTmpDir does not tell us anything about what is being set up in the target directory. I am proposing to use givenAnApplicationDir or setupAProjectDir or similar.
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.
+1 for givenAnApplicationDir
| * Not a member of the class because Yeoman will always call Generator | ||
| * prototype functions! Use .apply or .call with this function! | ||
| */ | ||
| function promptArtifactCrudVars() { |
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 make it a member function and check this.artifactInfo.controllerType === ControllerGenerator.CRUD to proceed.
| }) | ||
| .then(list => { | ||
| if (_.isEmpty(list)) { | ||
| return Promise.reject( |
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 call this.exit(error) to gracefully exit.
| self.artifactInfo.modelNameCamel = utils.camelCase( | ||
| self.artifactInfo.modelName | ||
| ); | ||
| return Promise.resolve(props); |
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.
No need to wrap props into a promise. A normal return value for then() will be converted to a promise automatically.
| export class <%= name %>Controller { | ||
| @inject('repositories.<%= modelNameCamel %>') <%= repositoryNameCamel %> : <%= repositoryName %>; | ||
| async create(obj: <%= modelName %>) : Promise<<%= modelName %>> { | ||
| return this.<%= repositoryNameCamel %>.create(obj); |
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 should be return await this.<%= repositoryNameCamel %>.create(obj);. The other option is to remove async keyword as no await is not in the method body.
|
|
||
| return this.prompt(prompts).then(props => { | ||
| Object.assign(this.artifactInfo, props); | ||
| return Promise.resolve(props); |
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.
No need to wrap props.
| * paths. Must return a Promise. | ||
| * @returns {Promise<string[]>} The filtered list of paths. | ||
| */ | ||
| exports.findArtifactPaths = function(path, artifactType, reader) { |
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.
These functions should be from @loopback/workspace in the future.
| .getArtifactList(self.artifactInfo.modelDir, 'model') | ||
| .then(list => { | ||
| if (_.isEmpty(list)) { | ||
| return Promise.reject( |
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 call this.exit(error) to gracefully exit.
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 seems that existence of models is required for lb4 controller to be useful. We probably need to add lb4 model soon.
raymondfeng
left a comment
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.
Please address my comments.
| self.artifactInfo.modelName, | ||
| self.artifactInfo.repositoryName, | ||
| ].forEach(item => { | ||
| const validationMsg = utils.validateClassName(item); |
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 set up validate function for each of the prompts instead of checking after all answers are accepted. The prompt level validator will reject invalid inputs before the next one. See an example at https://github.com/strongloop/loopback-next/blob/master/packages/cli/generators/app/index.js#L45.
4e1f53b to
652ee9d
Compare
|
@slnode test please (not sure why these jobs keep dropping...) |
| ].forEach(item => { | ||
| const validationMsg = utils.validateClassName(item); | ||
| if (typeof validationMsg === 'string') | ||
| throw new Error(validationMsg); |
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 set up validate function for each of the prompts instead of checking after all answers are accepted. The prompt level validator will reject invalid inputs before the next one. See an example at https://github.com/strongloop/loopback-next/blob/master/packages/cli/generators/app/index.js#L45.
64a8fce to
7c78581
Compare
|
@slnode test please |
| type: 'list', | ||
| name: 'id', | ||
| message: 'What is the type of your ID?', | ||
| choices: ['number', 'string', 'object'], |
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, we should be able to the type of model's id property automatically. Just saying, an explicit prompt is fine with me for MVP.
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 might need to build a separate module for the code that constructs the runtime type metadata so that we can share it across the juggler/relations, the openapi-spec and the CLI (since they're all going to need to know this information for some purpose).
bajtos
left a comment
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 stuff! I did not review all details, I focused on the most important parts only.
| import {<%= repositoryName %>} from '../repositories'; | ||
|
|
||
| export class <%= name %>Controller { | ||
| @inject('repositories.<%= modelNameCamel %>') <%= repositoryNameCamel %> : <%= repositoryName %>; |
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.
IMO, we should be using constructor injection, because we don't have any reasonable default to use for this repository if the user of our API does not provide us any. We are advocating for this approach in our best practices too, see http://loopback.io/doc/en/lb4/Implementing-features.html#decouple-controller-from-repository
If the class itself has a strong dependency upon the interface/object that you inject it with, you should use constructor injection as the class is pretty much useless without the dependency, i.e. it makes no sense to instantiate it without it. A constructor therefore enforces the dependency requirement.
But if it makes sense for the class to be able to do its job without using the dependency, you could use property injection. When using property injection the dependency may or may not used depending on whether the property is actually invoked.
So it depends.
Please refer to the following threads for more information about this:
http://stackoverflow.com/questions/1503584/dependency-injection-through-constructors-or-property-setters
http://stackoverflow.com/questions/21218868/setter-injection-vs-constructor-injection
http://programmers.stackexchange.com/questions/300706/dependency-injection-field-injection-vs-constructor-injection
packages/cli/package.json
Outdated
| "yeoman-test": "^1.7.0" | ||
| }, | ||
| "dependencies": { | ||
| "@types/util.promisify": "^1.0.0", |
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.
This shouldn't be needed, CLI is written in vanilla JavaScript.
See also our existing packages/core/src/promisify.ts.
packages/cli/test/test-utils.js
Outdated
| * structure. | ||
| * @param {string} tmpDir Path to the temporary directory to setup | ||
| */ | ||
| exports.setupTmpDir = function(tmpDir) { |
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.
Please use a more descriptive name. setupTmpDir does not tell us anything about what is being set up in the target directory. I am proposing to use givenAnApplicationDir or setupAProjectDir or similar.
packages/cli/lib/utils.js
Outdated
| 'use strict'; | ||
|
|
||
| // TODO(kjdelisle): Drop this shim once we drop Node 6. | ||
| require('util.promisify/shim')(); |
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.
So AFAICT, util.promisify/shim.js is modifying Node.js built-in util export - that's not good, but easy to fix by using util.promisify/implementation instead. It's also trying to support kCustomPromisifiedSymbol, but I don't think that will work unless the shim is loaded as the first thing in the app, so that 3rd party modules have access to this symbol. I guess that does not matter much since we are going to drop support for Node.js 6.x soon anyways.
As I am thinking about this, I guess as long as you use implementation instead of shim, so that we don't modify built-in require('util') then I am fine with your change. Please consider updating the existing promisify function in core to use the same implementation under the hood, so that we have consistent behaviour. Preferably as part of your PR (see also mine https://github.com/strongloop/loopback-next/pull/848/files#diff-9dab89a9fdf9229c03e541d55747f3bf)
packages/cli/test/utils.js
Outdated
| path.join('tmp', 'app', 'models', 'bar.js'), | ||
| path.join('tmp', 'app', 'models', 'README.js'), | ||
| ].concat(expected); | ||
| return Promise.resolve(result); |
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.
Why is it necessary to wrap the result in a promise? Can we fix utils.findArtifactsPaths to call Promise.resolve for us?
packages/cli/test/utils.js
Outdated
| return Promise.resolve(files); | ||
| }; | ||
| const artifactPath = path.join('tmp', 'app', folder); | ||
| it(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.
My experience with tests generated like this is mixed. Because it is called from a helper function, the (async) stack trace reported by a failed test does not show the place where the test suite was defined (where are the test parameters specified).
Can we refactor this helper to remove the call to it?
I am envisioning usage like this:
describe('getArtifactList', () => {
// ...
it('finds JS repositories', () => {
return verifyArtifactList(artifactType, folder, suffix, files, expected);
});
// ...
});I think it would be even better if we could move expect(results).to.eql(expected) out of this helper function too. That way the helper function is focused on executing getArtifactList in the given scenario, and everything else is up to the individual test to handle.
| utils.setupTmpDir(dir); | ||
| }) | ||
| .withPrompts(withInputProps) | ||
| .toPromise(); |
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.
This helper "before" hook seems to be duplicated at least in two places. Could you please extract it to a top-level helper shared by all describe suites?
55e4bec to
38fbb8b
Compare
bajtos
left a comment
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 updated code looks mostly good, please see few more comments below. Feel free to land the patch without waiting for another review from me, as long as somebody else approves your final version.
| validate: utils.validateClassName, | ||
| }, | ||
| ]; | ||
|
|
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: could you please preserve this empty line as a visual delimiter?
packages/core/src/promisify.ts
Outdated
| import * as util from 'util'; | ||
| // The @types/util.promisify conflicts with @types/node due to rescoping | ||
| // issues, so falling back to legacy import. | ||
| const promisifyPolyfill = require('util.promisify').implementation; |
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 it's better to require('util.promisify/implementation')?
packages/cli/lib/utils.js
Outdated
| const regenerate = require('regenerate'); | ||
| const _ = require('lodash'); | ||
| const pascalCase = require('change-case').pascalCase; | ||
| const promisify = require('util.promisify/implementation'); |
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.
Oh, sorry for a misleading comment. Your code here will use the user-land promisify implementation even on Node.js 8.x where a native version is available.
Here is a better solution:
const promisify = util.promisify || require('util.promisify/implementation');
// ...
exports.promisify = promisify;(I am proposing to export the promisify helper so that I can easily access it in my #848 too).
packages/cli/test/controller.js
Outdated
| path.join(tmpDir, 'package.json'), | ||
| JSON.stringify({ | ||
| keywords: ['foobar'], | ||
| return runGenerator(true, true).then(() => { |
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.
Huh, what does true, true mean and how does it differ from false, true? Boolean arguments are evil, please used an options object with named properties.
runGenerator({useTempDir: true, withPrompts: 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 knew I should've gone with my gut instinct!
packages/cli/test/controller.js
Outdated
| JSON.stringify({ | ||
| keywords: ['loopback'], | ||
| }) | ||
| path.join(tmpDir, 'src', 'repositories', 'bar.repository.ts'), |
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 extracting a helper function for this? e.g.
givenRepositoryScript('bar.repository.ts');
givenModelScript('foo.model.ts');
packages/cli/test/controller.js
Outdated
| if (useTempDir) { | ||
| result = result.inTmpDir(dir => { | ||
| utils.givenAnApplicationDir(dir); | ||
| }); |
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.
result = result.inTmpDir(util.givenAnApplicationDir);Unless givenAnApplicationDir is assuming this = utils?
packages/cli/test/controller.js
Outdated
| const helpers = require('yeoman-test'); | ||
| const fs = require('fs'); | ||
| const util = require('util'); | ||
| const utils = require('./test-utils'); |
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 difference between util vs. utils is easy to overlook. Could you please rename the second import from utils to testUtils?
| let modelList = []; | ||
| let repositoryList = []; | ||
| if ( | ||
| !this.artifactInfo.controllerType || |
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, is promptArtifactCrudVars for "Basic CRUD controller" only or for any other controllers except artifactInfo.controllerType === ControllerGenerator.BASIC?
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'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
3f2b195 to
f5e883b
Compare
|
@raymondfeng PT(another)L, I think it's almost ready to land. |
|
@slnode test please |
f5e883b to
0c880b2
Compare
| } | ||
| return utils | ||
| .getArtifactList(this.artifactInfo.modelDir, 'model') | ||
| .then(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.
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'
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've added a .catch at the end of the chain to call this.exit (it won't mark this comment as outdated)
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.
Unfortunately it does not work as your added catch only applies to return this.prompt([. I still see the same issue with your changes.
0487fe9 to
ec52c3c
Compare
|
@kjdelisle Some of the assertions fail on Windows. Please fix them. |
ab81256 to
514debb
Compare
514debb to
b83a0fb
Compare
|
|
||
| @post('/<%= modelNameCamel %>') | ||
| @param.body('obj', <%= modelName %>) | ||
| async create(obj: <%= modelName %>) : Promise<<%= modelName %>> { |
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.
Please fix all methods to use parameter level decorators.
async create(@param.body('obj', <%= modelName %>) obj: <%= modelName %>) : Promise<<%= modelName %>> {
return await this.<%= repositoryNameCamel %>.create(obj);
}|
|
||
| constructor( | ||
| @inject('repositories.<%= modelNameCamel %>') | ||
| @inject('repositories.<%= repositoryName %>') |
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.
Quick note: Was testing this on a real application and noticed that we must've changed our approach awhile ago, since the RepositoryMixin is now creating repositories with the name of the constructor object. Updated the template to reflect this so that it works out-of-the-box.
Description
CLI command for generating controllers now supports the creation of a CRUD controller template.
Implements #727 (Part 1 of 2)
Checklist
npm testpasses on your machinepackages/cliwere updatedpackages/example-*were updated