-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(cli): add prompt for plural model name #1314
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
| when: this.artifactInfo.modelName === undefined, | ||
| default: response => | ||
| utils.kebabCase(utils.pluralize(response.modelName)), | ||
| validate: utils.validateUrlSlug, |
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 prompt would look something like this:
What is the name of the model to use with this CRUD repository? Customer
What is the plural form of the model? (customers)When given a bad input (foo#bar):
What is the plural form of the model? (customers)
Invalid url slug. Suggested slug: foo-barThere 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.
Once we rephrase the question to ask for URL path, should we include the leading / in the default value?
If we do so, I think we should still accept and normalize values without a leading slash. For example:
- User enters "foo-bar" -> we use
/foo-bar - User enters "foo/" -> we use
/foo
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 decided to not add in any flags for plural form. Flags for setting model and repository don't exist, and I don't think we want users to specify setting a specific plural name for any potential models they may have; the flag should only be able to be set if a flag for the model (that they want to set the plural name on) can also be set.
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.
Please revert the changes of /todo to /todos in the docs and example repose - see the comments below.
| ) {} | ||
|
|
||
| @post('/todo') | ||
| @post('/todos') |
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 the singular name in the path was intentional here. POST /todo means "create a new TODO instance". POST /todos can be interpreted as "create many TODO instances".
| } | ||
|
|
||
| @get('/todo/{id}') | ||
| @get('/todos/{id}') |
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.
Similarly here. In REST, it's common and natural to have two path namespaces for a single model - /todo to work with a single model instance, /todos to work with the collection.
Such design has an important property: it avoids naming clashes between instance ids and remote method names - for example, it's possible to have todo with id findOne (available at GET /todo/findOne) and an remote method called findOne (exposed at GET /todos/findOne). This is something we cannot do in LB 3.x.
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.
With that in mind, I'm wondering whether we should modify our template to 'correct' the designs of our available controller methods. Putting this into example, if given path url was todo, should findById be tied to /todo/{id} while find is tied to /todos?
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.
That would be perfect!
My only concern is about the UX and implementation complexity of such solution. I think we will have to prompt for both paths (/todo and /todos), explain the user the differences, and allow them to use the same path for both types of endpoints.
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.
for example, it's possible to have todo with id findOne (available at GET /todo/findOne) and an remote method called findOne (exposed at GET /todos/findOne). This is something we cannot do in LB 3.x.
👍
My only concern is about the UX and implementation complexity of such solution
I think now we don't append namespace prefix to the path of controller method, e.g. @post('/todo') or @post('/todos'), user need to provide the completed path, maybe we can enhance the REST decorators to allow specifying the namespace prefix:
- plural
- singula
- customized
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 UX of this approach does seem difficult to grasp. Here are all of the possible options that may be available to the users:
When given Todo model,
- Use
todoandtodos - Use just
todo - Use just
todos - Custom http path
- Use only the custom path given
- Prompt for custom singular path
- Use plural form of given answer
- Custom plural form
As you can see, there are tons of options that can be available to the users and if we were to go with this approach, we should only pick a couple of these to avoid overloading users with choices.
@strongloop/lb-next-dev What do you think?
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 suggestion in #1314 (comment) is totally a different story :) As for this PR I would like to just focus on adding the prompt.
For the singular/plural namespace, my take would be
- use singular for the item takes in param with type
Todo - use plural for those take in param with type
Todo[]
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.
For reference, this is how we handle this in LB3. I also like the idea of following REST principles in our templates.
For the singular/plural namespace, my take would be
- use singular for the item takes in param with type Todo
- use plural for those take in param with type Todo[]
if this can be easily done, then that would make our UX easier and then we are left with a custom http path prompt. Maybe we can compromise between UX and flexibility.
docs/site/Controller-generator.md
Outdated
| to select or specify: | ||
|
|
||
| - The model to use for the CRUD function definitions | ||
| - The plural form of model to use for REST paths |
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.
In my experience, the whole thing with plural model forms was rather confusing in LB 3.x. The connection from a plural model form to an HTTP path mapping is not obvious, plus there are examples where a plural form is confusing to people with a non-English primary language (e.g. a Person model is exposed at /people).
Also in LB4, it is possible to have a controller that's not tied to a single model (or any model at all), in which case the plural form makes even less sense.
I am proposing to be explicit about our intents in LB 4.x. When we need to decide how to map model's CRUD methods to a REST API, then ask the user about the base HTTP path to use. The fact that we use a plural form as the default path should be just our implementation detail.
| type: 'input', | ||
| name: 'modelNamePlural', | ||
| message: | ||
| 'What is the custom plural form of the model (for building REST URL)', |
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.
As I commented above, please rephrase this question to make it clear that we are asking for the URL base path.
| 'What is the custom plural form of the model (for building REST URL)', | ||
| when: this.artifactInfo.modelName === undefined, | ||
| default: response => | ||
| utils.kebabCase(utils.pluralize(response.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.
Nice, +1 for using kebabCase.
Here is a wider context:
URLs like /ProductReviews are an anti-pattern in the REST world. The convention is to use kebab case, e.g. /product-reviews.
In LB 3.x, we didn't want to break all existing applications, therefore we never changed the legacy behavior. There is a flag to enable kebab case though, see rest.
normalizeHttpPath.
I'd like us to leverage the opportunity to break backwards compatibility that LB 4.0 offers us, and fix the default REST APIs to finally use kebab-case instead of CamelCase in URL paths.
The impact on this pull request should be pretty minimal.
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.
Actually, since you added a dependency on url-slug, I think we should use that one instead of kebabCase. What do you think?
| when: this.artifactInfo.modelName === undefined, | ||
| default: response => | ||
| utils.kebabCase(utils.pluralize(response.modelName)), | ||
| validate: utils.validateUrlSlug, |
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.
Once we rephrase the question to ask for URL path, should we include the leading / in the default value?
If we do so, I think we should still accept and normalize values without a leading slash. For example:
- User enters "foo-bar" -> we use
/foo-bar - User enters "foo/" -> we use
/foo
| ) {} | ||
|
|
||
| @post('/<%= modelNameCamel %>') | ||
| @post('/<%= modelNamePluralKebab %>') |
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.
Our variable names should reflect the intent (what is the URL path), not the implementation details (a plural+kebab version of the name). Let's rename this variable to something like modelUrlPath please.
| 'What is the custom plural form of the model (for building REST URL)', | ||
| when: this.artifactInfo.modelName === undefined, | ||
| default: response => | ||
| utils.kebabCase(utils.pluralize(response.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.
Actually, since you added a dependency on url-slug, I think we should use that one instead of kebabCase. What do you think?
| generator, | ||
| Object.assign( | ||
| { | ||
| modelName: 'Foo', |
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 multi-word name to verify how it is converted to an URL. It's also better to use real-world names instead of generic Foo.
A proposed model name: ProductReview.
dbe1eec to
2d4b2c6
Compare
| } | ||
| }); | ||
|
|
||
| describe('validateUrlSlug', () => { |
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.
With the way validateUrlSlug is currently coded, the function accepts camel casings along with other words separated by url-safe separators (such as _). Should we potentially look to enforce kebab-casing?
docs/site/Controller-generator.md
Outdated
| @patch('/todos') | ||
| async updateAll( | ||
| @param.query.string('where') where: Where, | ||
| @reqeustBody() obj: Todo, |
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.
Todo[]
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.
https://github.com/strongloop/loopback-next/blob/master/packages/repository/src/repositories/legacy-juggler-bridge.ts#L169
Seems like updateAll only takes in a single instance of Todo
| @patch('<%= httpPathPlural %>') | ||
| async updateAll( | ||
| @param.query.string('where') where: Where, | ||
| @requestBody() obj: <%= 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.
objs: <%= 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.
Posting what I posted above:
https://github.com/strongloop/loopback-next/blob/master/packages/repository/src/repositories/legacy-juggler-bridge.ts#L169
Seems like updateAll only takes in a single instance of Todo
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.
IIRC, updateAll works more like patchAll, it accepts a single key/value object containing changes to apply to all model instances matching the "where" filter. The correct type definition would be Partial<ProductReview> (replace ProductReview with the actual 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.
oh sorry, my bad, @bajtos is correct ^
👍
8f8cf80 to
2d7156a
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.
LGTM, please consider my comments below before merging.
packages/cli/lib/utils.js
Outdated
| * Adds a backslash to the start of the word if not already present | ||
| * @param {string} httpPath | ||
| */ | ||
| exports.appendBackslash = httpPath => httpPath.replace(/^\/?/, '/'); |
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.
"append" usually means to add content at the end of the string. I am proposing to use "prepend" instead, i.e. prependBackslash.
| type: 'list', | ||
| name: 'httpPathNameChoices', | ||
| message: | ||
| 'Select the HTTP path naming convention of this CRUD repository', |
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, this prompt is redundant. Since the next two prompts (httpPathSingular and httpPathPlural) are already offering default values, users can tell us to use the default convention by simply hitting Enter key twice to accept both proposed defaults.
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.
Although I agree, I personally like that users would have to only hit Enter once in most cases with this approach. I also think the prompt is well organized in sense that first-time users don't have to stop to think about what singular HTTP path name is supposed to mean.
Not really against your suggestion though. Thoughts @strongloop/lb-next-dev ?
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.
first-time users don't have to stop to think about what singular HTTP path name is supposed to mean.
This is a good point that I haven't considered. Let's keep the initial prompt for the naming convention then.
| @patch('<%= httpPathPlural %>') | ||
| async updateAll( | ||
| @param.query.string('where') where: Where, | ||
| @requestBody() obj: <%= 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.
IIRC, updateAll works more like patchAll, it accepts a single key/value object containing changes to apply to all model instances matching the "where" filter. The correct type definition would be Partial<ProductReview> (replace ProductReview with the actual modelName).
|
Great design! I really like the simple vs. customization approach allowing the user to power through the prompts. I'm not a fan of the Great work :) |
|
Sorry, I found the prompts and mixed usage of singular and plural forms for base paths for the same controller very confusing. Here is an example of generated controller class: export class TestController {
constructor(
@repository(CustomerRepository)
public customerRepository : CustomerRepository,
) {}
@post('/customer')
async create(@requestBody() obj: Customer)
: Promise<Customer> {
return await this.customerRepository.create(obj);
}
@get('/customers/count')
async count(@param.query.string('where') where: Where): Promise<number> {
return await this.customerRepository.count(where);
}
@get('/customers')
async find(@param.query.string('filter') filter: Filter)
: Promise<Customer[]> {
return await this.customerRepository.find(filter);
}
@patch('/customers')
async updateAll(
@param.query.string('where') where: Where,
@requestBody() obj: Customer
): Promise<number> {
return await this.customerRepository.updateAll(where, obj);
}
@del('/customers')
async deleteAll(@param.query.string('where') where: Where): Promise<number> {
return await this.customerRepository.deleteAll(where);
}
@get('/customer/{id}')
async findById(@param.path.number('id') id: string): Promise<Customer> {
return await this.customerRepository.findById(id);
}
@patch('/customer/{id}')
async updateById(
@param.path.number('id') id: string,
@requestBody() obj: Customer
): Promise<boolean> {
return await this.customerRepository.updateById(id, obj);
}
@del('/customer/{id}')
async deleteById(@param.path.number('id') id: string): Promise<boolean> {
return await this.customerRepository.deleteById(id);
}
}
|
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.
I think the PR is heading to a wrong direction.
|
@raymondfeng I think it all depends on stylistic choices we want to advocate to users. From what I've seen on the net, people are split on the design here; some people think it's good to have the endpoints make sense linguistically (e.g. getting an ID of a resource should be singular) while others expect the identifier to be the same for the operations done with that resource. I see both sides of the argument and don't have a strong opinion for either and wouldn't be opposed to reverting the singular/plural distinction. @bajtos, do you have a strong opinion on having the REST path be linguistically correct? |
|
Now that I think about the |
|
The discussion is folded: #1314 (comment) cc @raymondfeng for your comment, see the related discussion ^^ |
I never see any REST APIs use mixed forms of singular and plural base paths for the same The purpose of the prompt should be just getting a consistent base path for the controller. We derive a default value from the model name for confirmation/customization. |
|
Sorry, I’m a bit late in the game. But having to use different base paths for the same controller/resource is really confusing. Think about the basePath as the |
|
You've convinced me. I'll be changing the PR to have all the operations available under the same base path |
At some point in the past, GitHub used to have different URLs for accessing a collection of resources and an individual resource. For example:
It looks like they have moved away from that pattern since then, their v3 API uses the same plural form now - see https://developer.github.com/v3/pulls/ In that light, I am fine with using the same base path for all controller methods, as proposed by Raymond. Sorry for derailing this pull request and wasting our time on a wrong direction 🙈 I have one concern though which may be orthogonal to the discussion here, but I'd still like to raise it: with a single base path for all controller method, we have a shared namespace for model ids and remote method names. Consider the case where models are identified by string names in the REST API, for example we may have the following model-instance paths:
In addition to instance-specific paths, we have also remote methods:
Now what happens when an API client creates a resource with id With two paths (singular/plural), it's easy to create the convention that all remote methods are exposed on the plural path ( I don't see how to solve this challenge when there is only a single base path for all resource-related methods. We could require resources to avoid using IDs that match remote method names, but I find that rather impractical, considering the list of reserved IDs can change as the code evolves. Thoughts? |
|
The possibility of conflict between ids and our built-in operation names such as For the edge case, I see a few options:
@bajtos If you are keen on this, please open an issue to track it. |
Yeah, I agree. I guess I just don't like the possibility of a naming conflict that the current solution offers, I prefer to design my code/apps to make such conflicts impossible instead of only low-probability. Not a big deal, we can wait until we have a real world scenario when this matters. |
16b19d4 to
34202ff
Compare
| - The repository for this model that provides datasource connectivity | ||
| - The REST path naming convention | ||
| - Default: singular and plural forms of the model name in dash-delimited style | ||
| are used |
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.
Since we are using plural form everywhere, this no longer describes the actual implementation. Could you please fix it @shimks?
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.
Yup, I'll open a PR to fix this
UX:
If
customis selected:Checklist
npm testpasses on your machinepackages/cliwere updatedexamples/*were updated