-
Notifications
You must be signed in to change notification settings - Fork 1.1k
[PoC - WIP] very rough PoC on request parameter type coercion/validation #1256
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
6b40a33 to
3f721fd
Compare
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.
@shimks reviewing the first commit from you(feature: do a very short PoC on type coercion):
Nice start!
The overall implementation looks reasonable to me 👍 : invoke the coerce in parser, or maybe we can have a separate action for it after parser,
IMO two things we should consider as implementation is:
- the context we need when invoke coerce
- take options in the coerce function.
- we need the type coercion for both input and output:
- input: invoke in/after parser
- output: invoke in/before writer
|
@jannyHou what type of options should we consider when doing coercion? |
49d9b52 to
f3dbd67
Compare
|
@shimks It's just a very general option param that leave as the last parameter of a coercion function. |
1bbba35 to
afa4262
Compare
| // License text available at https://opensource.org/licenses/MIT | ||
|
|
||
| const nodeMajorVersion = +process.versions.node.split('.')[0]; | ||
| module.exports = nodeMajorVersion >= 7 ? require('./dist') : require('./dist6'); |
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 no longer support Node.js 6.x, where does this line come from? Could you please use the latest project infrastructure, e.g. as scaffolded by lb4 extension?
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 PR builds on top of #947, which was proposed before we dropped Node 6. I didn't bother with pretty easily fixable stuff like this one to focus on the subject of the PoC itself. If we decide to go forward with the PoC, I (or someone else building on top of the PoC) will definitely need to fix this.
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.
Agreed 👍
| @validate({format: 'email'}) | ||
| str: string, | ||
| @param.path.number('num1') | ||
| @validate({ |
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 introducing another decorator @validate. IMO, developers should define validation rules as part of the OpenAPI Spec parameter definition.
@param.path('num1', {type: 'number', multipleOf: 5});That way the OpenAPI spec of the application include all additional constraints imposed on method arguments.
If there are use cases for using @validate outside of REST/OpenAPI context, then I am fine with having @validate decorator, as long as @param decorators invoke the @validate decorator under the hood.
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 agree with you. The PoC was to show that we can use @validate as a base for openapi-v3 decorators, and potentially any other specs like for GraphQL. I didn't have the time to work on the integration itself with openapi-v3, but that doesn't seem like something that's difficult to do (call @validate on converted spec from @param, and call @validatable after @operation)
| } | ||
| // tslint:disable-next-line:no-any | ||
| descriptor.value = async function(...args: any[]) { | ||
| const ajv = new AJV(); |
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 using AJV for validations.
To get the best performance, we need to cache compiled validators and reuse them across requests. Please consider this requirement in your design. See https://www.npmjs.com/package/ajv#performance and https://www.npmjs.com/package/ajv#compileobject-schema---functionobject-data.
| if (isSchemaObject(paramObject.schema)) { | ||
| // basic type | ||
| const serializer = getSerializer(paramObject.schema.type!); | ||
| coercedParamArgs.push(serializer.coerce(paramArgs[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.
I have mixed feelings about rolling out our own coercion implementation. At one hand, it allows us to use the same coercion algorithm in multiple places (e.g. REST, ORM/database, etc.) and have full control over the coercion rules. At the other hand, there are many edge cases that are tricky to get right and we can easy end up with a behavior that's surprising for users because it's inconsistent with other coercion implementations (LB 3.x, AJV coercion).
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, I don't think there is a popular coercion package that we can leverage to fit to our needs.
I feel coercion rules from string to whatever seem consistent for most coercion implementations. I like your suggestion of having a comprehensive testing suite in your main comment down below and I think it will make for a good starting point.
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 this context, it's probably better to talk about deserialization. The responsibility is to extract typed/formatted values from http requests (a set of strings from path/query/header/body/...).
| // tslint:disable-next-line:no-any | ||
| descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, | ||
| ) { | ||
| const originalMethod = descriptor.value; |
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 am not a fan of decorators that are directly modifying properties and methods of the decorated objects, I think the complexity of handling edge cases (like the number of arguments stored in .length property) is not worth the benefits.
What are the arguments for using this approach, as opposed to letting the REST layer to perform the actual validation based on metadata provided by @param and/or other metadata-only decorators?
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 main point for using this approach is for the clean error tracing. Our sequence makes the error tracing irrelevant, but I personally thought having the arguments validated right as the controller method is invoked seemed like it just made sense at high level.
Another argument for it is that the decorators would be automatically compatible with any future server extensions. This approach would let extension developers implement validation for their servers just by having the users import the package.
As for the edge cases, the only ones I see aside from length are name and toString(). Are there any that I may have missed?
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 see your points.
To me, replacing controller methods seems too magic, I prefer to have more control. As I was envisioning validation, different server extensions will call the validation step in their transport-specific sequence and each server extension will decide when is the right time to perform the validation and coercion.
Few things to consider:
Does it make sense to split coercion and validation into two steps? Are these steps really independent? Consider the REST sequence where parseParams action is responsible for extracting typed arguments from the HTTP request (and thus coerce the values from string sources like query string values to correct types). If the validation happens inside invoke action, then how are we going to treat invalid input values? Are we going to perform a partial validation as part of coercion, and then another validation as part of invoking controller method?
Let's say a controller method accepts an argument of type "unsigned 32bit integer" and the caller provided a string (e.g. foo) or a value that's out of bounds (e.g. -1 or 17e10). Using the default sequence (see below), what should parseParams return for this input argument? https://github.com/strongloop/loopback-next/blob/43383f5ff8ab98880e5a3bdb91d91c03e10c5ba5/packages/rest/src/sequence.ts#L106-L108
In my opinion, both coercion and validation should happen at the same time.
|
Coercion is a tricky business and we have been bitten by many user-reported issues back in the LoopBack 2.x days. When I was reworking this area for LoopBack 3.x, I came to the conclusion that we need to apply different coercion algorithm depending on where the value comes from:
For example, when coercing a Date from a JSON value, we may want to treat Consider also To implement a good type & coercion library, we need an extensive test suite covering all different edge cases. See the description in strongloop/strong-remoting#312 for a glimpse and all test files in strong-remoting/test/rest-coercion to see all edge cases to consider. Other related issues that may be helpful here:
|
|
@shimks is this pull request still relevant? Can we close it? |
|
Nope, closing it |
Note: this is NOT a comprehensive PR which covers all aspects and features we need in coercion and validation; it is merely a proof of concept to highlight certain approaches we can take and have room to be extended out into a full blown feature-complete PR. This PR will eventually be closed in favor of feature-separate PRs.
@operationand@param/@requestBodybeing used to infer the parameter type on the controller@loopback/types) choosing the 'LoopBack' type from the metadata which will be used to coerce the request data@validatableand parameter level decorator@validateare used to do validation of parameters using JSON SchemasChecklist
npm testpasses on your machinepackages/cliwere updatedexamples/*were updated