-
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: coerce query object with schema #5808
Conversation
@@ -98,9 +115,9 @@ describe('Coercion', () => { | |||
// This is because all values are encoded as strings on URL queries | |||
// and we did not specify any schema in @param.query.object() decorator. | |||
where: { | |||
id: '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.
Do we want to add fields
filter to this test case? You might want to remove the comment above as it is fixed.
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.
@agnes512 I started adding feature by manually verify the field filter in todo app, so the fields filter does work.
where: {active: true}
and
fields: {name: true}
doesn't make any difference for testing purpose, so I would say the current test is good enough to cover our case(field filter) :p
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 I mentioned it because I remember when I was testing it, it works for where
for some reasons 😂
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.
@agnes512 I removed my change and tested with todo app, filter ?[where][isComplete]=false
does return correct result for some reason, weird.
But the filter it self is not coerced, see my screenshot in:
isComplete is "false"
not false
.
I need more time to investigate the reason, but the updated test case here should be correct. For the parameter coerce function, it wouldn't make sense if
where: {active: true}
get coerced while fields: {name: true}
not.
const schema = extractSchemaFromSpec(spec); | ||
if (schema) { | ||
// apply coercion based on properties defined by spec.schema | ||
const ajv = new Ajv({coerceTypes: 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.
It's a bit concerning that we hard-code the instantiation of Ajv
here. We typically want to inject the ajv options.
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.
@raymondfeng The ajv here is not used for validation, but for coercion purpose, I know this sounds a bit weird, but I couldn't find a better&simpler way to coerce based on schema.
See ajv doc in https://github.com/ajv-validator/ajv#coercing-data-types
If you search options
in file coerce parameter, there is no options available for coercing 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.
We should add an optional options
parameter for coerceParameter
. Its main usage is in parse.ts
which has access to the configuration.
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.
@raymondfeng Let me submit another PR or at least a separate commit for options
.
@@ -18,6 +23,7 @@ import { | |||
matchDateFormat, | |||
} from './utils'; | |||
import {Validator} from './validator'; | |||
const Ajv = require('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.
Please use import
instead of require
.
if (schema) { | ||
// apply coercion based on properties defined by spec.schema | ||
const ajv = new Ajv({coerceTypes: true}); | ||
const validate = ajv.compile(schema); |
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 not create the Ajv
instance per call. It's fairly expensive.
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.
True, I refactored to use one global instance. Or should I use a singleton scope provider instead?
packages/rest/src/__tests__/acceptance/coercion/coercion.acceptance.ts
Outdated
Show resolved
Hide resolved
const schema = extractSchemaFromSpec(spec); | ||
if (schema) { | ||
// Apply coercion based on properties defined by spec.schema | ||
const validate = AJV.compile(schema); |
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.
See more details at #1757. The compile
call is probably the most expensive one.
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 probably have to maintain a weak map (schema -> ajv).
* The AJV instance is created for coercing object parameters | ||
* with provided schema. Not for validation. | ||
*/ | ||
const AJV = new AjvCtor({coerceTypes: 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.
We have an AjvFactory
being injected and it's available via options
.
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.
IIUC, the observed behavior is caused by the fact that we are not validating non-body parameters yet, see #1573 and linked PRs that attempted to solve that, but were never finished.
Instead of trying to add coercion-only step and having to deal with problems like how to obtain the AJV instance in a way that's consistent with the existing REST-layer validations, can we please implement proper validation of all non-body parameters and thus get coercion for "free"?
Even though my old TODO comment says coerceObject
should take into account spec.schema
, I think it maybe better to keep coerce-parameter
implementation schema-less and leave it up to the validation step to perform additional coercion as needed.
const schema = extractSchemaFromSpec(spec); | ||
if (schema) { | ||
// Apply coercion based on properties defined by spec.schema | ||
await validateValueAgainstSchema( |
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.
validateValueAgainstSchema
throws an error when AJV validation fails. Is it ok for coerceObject
to throw validation errors?
validateValueAgainstSchema
prints debug logs referring to request body
. If we are going to use validateValueAgainstSchema
for non-body parameters (which consider as a good idea), then the debug logs need to be updated accordingly.
We should also take a look at the validation errors produced for non-body parameters and make sure they provide enough information about which parameter failed the validation.
Another aspect to consider: should we report the first validation error only (I think that's the current design)? IMO, it's easier for users when all errors are reported at once. A possible solution for this is to build a "composite" object from all parameters (e.g. {filter: <value from query string>, authorization: <value from headers>}
), a "composite" schema from Operation Object, and then call AJV only once. I think this way the error messages will include the parameter name ("filter", "authorization") as part of the description (path to the property with a wrong 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.
Having an aggregated schema that stitches all incoming params (including the body) is an interesting idea that we should explore. But let's leave it out of scope for this PR.
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.
Having an aggregated schema that stitches all incoming params (including the body) is an interesting idea that we should explore.
@bajtos @raymondfeng Thanks for suggestion, I also tend to create a separate story for it. In this PR, I turned validateValueAgainstSchema
a generic function that validates any value:
- rename 1st parameter:
data
-->value
ValueValidationOptions
extendsRequestBodyValidationOptions
with one more propertyposition
- throws invalid request body error when
position
is 'body', throws invalid data error otherwise.
The new story can create a new rest http error for other invalid data.
WDYT?
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.
creating the follow-up story, post here soon.
@@ -94,13 +111,10 @@ describe('Coercion', () => { | |||
}) | |||
.expect(200); | |||
sinon.assert.calledWithExactly(spy, { | |||
// Notice that numeric and boolean values are converted to strings. | |||
// This is because all values are encoded as strings on URL queries | |||
// and we did not specify any schema in @param.query.object() decorator. |
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 preserve the "notice" comment and update it to describe (point out) the new behavior.
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 the second though, I think we should preserve the current test as-is to describe how are schema-less values handled, and add a new test to verify how does schema-based coercion work.
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 we should preserve the current test as-is to describe how are schema-less values handled, and add a new test to verify how does schema-based coercion work.
Good point, I will edit the test.
@@ -83,7 +83,7 @@ async function buildOperationArguments( | |||
} | |||
const spec = paramSpec as ParameterObject; | |||
const rawValue = getParamFromRequest(spec, request, pathParams); | |||
const coercedValue = coerceParameter(rawValue, spec); | |||
const coercedValue = await coerceParameter(rawValue, spec); |
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.
Building on top of my previous comments, perhaps this is the place where to call validation?
const coercedValue = await coerceParameter(rawValue, spec); | |
const coercedValue = coerceParameter(rawValue, spec); | |
const finalValue = await validateParameterValue(rawValue, spec); | |
paramArgs.push(finalValue); |
(Of course, this will not help with collecting all validation error together and other aspects mentioned in my other comments, but it may be good enough as a short-term fix.)
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 have also access to globalSchemas
and options
here, which is needed to resolve {$ref}
schemas and use the same AJV factory as the request body validator does.
const coercedValue = await coerceParameter(rawValue, spec); | |
const coercedValue = await coerceParameter(rawValue, spec); | |
const coercedValue = coerceParameter(rawValue, spec); | |
const finalValue = await validateParameterValue(rawValue, spec, globalSchemas, options); | |
paramArgs.push(finalValue); |
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.
Thanks @bajtos I tried calling validateValueAgainstSchema
in the new place, and got 20+ test failures. This story is to coerce the object based on schema, but coerceParameter
handles all types, and same to validateValueAgainstSchema
if I call it after coerceParameter
.
I am a bit reluctant to investigate and verify why other types fail, and considering schema
is usually used for object, can we deal with other types next time?
@@ -115,7 +115,7 @@ function getKeyForOptions(options: RequestBodyValidationOptions) { | |||
* @param globalSchemas - Schema references. | |||
* @param options - Request body validation options. | |||
*/ | |||
async function validateValueAgainstSchema( | |||
export async function validateValueAgainstSchema( |
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 change the reference to body
with a generic one, such as 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.
done.
f3d89d4
to
b545786
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.
Not sure what validateValueAgainstSchema
does but the rest LGTM 👍
Connect to #4992
Filter with non-string properties like
?filter[fields][name]=false
are not coerced, sofalse
turns to be'false'
. This PR applies coercion for parameter query.Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated👉 Check out how to submit a PR 👈