-
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: localize error in details #1511
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.
Does it make sense to remove the leading .
in the dataPath
property ... for example for .isComplete
=> isComplete
.
{ | ||
description: 'missing required "title"', | ||
description: 'a todo missing required field title', |
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.
Shouldn't title
be in quotes?
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
packages/rest/src/rest-http-error.ts
Outdated
return new HttpErrors.UnprocessableEntity(msg); | ||
export function invalidRequestBody(): HttpErrors.HttpError { | ||
return new HttpErrors.UnprocessableEntity( | ||
'The request body is invalid. See error object `details` property for more info.', |
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.
Shall we extract out the message into a variable like the other functions above?
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.
👍 definitely.
validateRequestBody(body, spec, schemas); | ||
throw new Error('function `validateRequestBody` should throw error.'); | ||
} catch (err) { | ||
expect(err.message).to.equal(errorMatcher); |
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 use shouldjs's throw
to assert for properties in the error 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.
@shimks shoudjs doesn't support a customized error assertion function, that's why I have to take this workaround. I need to do a deep equal check for error.details
.
And I also tried do the assertion inside a expect.to.throw
, like this:
function shouldThrowError() {
try {
throwError();
} catch (err) {
// assertion 1
expect(err.details).to.deepEqual(expectedDetails);
throw err;
}
}
// assertion 2
expect(shouldThrowError).to.throw(expectedMessage)
Its problem is if assertion 1 fails with message1
, assertion 2 will receive message 1
instead of the expectedMessage
thrown from throwError()
, then assertion 2 fails too(but it shouldn't), and prints a very unreadable message, sth like:
// message 1 is a super long error log for a deepEqual assertion
message 1 doesn't match expectedMessage
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.
@jannyHou When you say shouldjs does not support customized error assertion, do you mean that it can't take in an additional object with key/value pairs that may exist inside the error object, or that it can't do deep assertions on the properties? Because throw()
can take in a second argument with the customized properties: https://shouldjs.github.io/#assertion-throw
I'm trying to push for the throw()
so that the code doesn't have to manually throw an error if no error was thrown. I guess it's not a big deal 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.
IIUC, @jannyHou is saying that Assertion#throw
is not performing a deep equality check on error properties, in which case I am fine with the current solution.
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 thanks for the doc 👍 will try it if I have time but like Miroslav said, it probably couldn't do a deep equality check. will let you know 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.
@shimks even I provide {details: details}
in the 2nd input like:
function validate() {
validateRequestBody(body, spec, schemas);
}
expect(validate).to.throw(errorMatcher, {
// tests still pass if you change the code to `details: {}`
details: details,
});
it doesn't do the equality check, but only checks the error message match. Tests still pass if you change the code to details: {}
.
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 correct usage would be a single-arg invoke of throw
:
expect(validate).to.throw({
message: /* message text (strict comparison? not ideal) */
details: details
});
Please note that {details: details}
can be shortened to {details}
, and also don't forget to await
the expect statement.
await expect(validate).to.throw({details});
// or
await expect(validate).to.throw({
message: /*...*/,
details,
});
I don't really mind implementation details of verifyValidationRejectsInputWithError
as long as it works correctly in all cases. As I wrote earlier, the current implementation is fine with me.
@virkt25 thanks for the feedback:
I do feel a little bit weird when first saw it...while on a second thought, I think it's a good implication of "a property of object", some type like array data doesn't have that dot, see example in this test |
Based on #1511 (comment), I also have some thought about the naming conversion of ajv error metadata:
Not a strong opinion and let's discuss after agreeing on the signature :). |
f4f2113
to
e3af709
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.
params: I prefer to use additionalInfo or sth like it that starts with additional*
I agree with naming this key additionalInfo
.
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 at high level.
I would like to see the information about details
structure from the pull request description captured in our docs eventually (as part of DP3). The docs should mention things that may be surprised to our users, e.g. that required properties have dataPath
pointing to the owning object, not the missing property.
I also have some thought about the naming conversion of ajv error metadata:
dataPath: sounds straightforward enough to me
+1
Maybe path
would be even better?
keyword: I prefer to use code instead...
+1 to use code
message: lgtm
+1 to keep message
params: I prefer to use
additionalInfo
or sth like it that starts with additional*
How about just info
or data
? Not a big deal, additionalInfo
is still better to me than params
.
packages/rest/src/rest-http-error.ts
Outdated
} | ||
/** | ||
* An invalid request boby error contains a `details` property as the machine-readable error. |
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.
typo: boby
packages/rest/src/rest-http-error.ts
Outdated
} | ||
/** | ||
* An invalid request boby error contains a `details` property as the machine-readable error. | ||
* Each entry in `error.details` contains the following attributes: |
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 is the type of error.details
- is it an array or an object? It would be great to mention the type here too.
validateRequestBody(body, spec, schemas); | ||
throw new Error('function `validateRequestBody` should throw error.'); | ||
} catch (err) { | ||
expect(err.message).to.equal(errorMatcher); |
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, @jannyHou is saying that Assertion#throw
is not performing a deep equality check on error properties, in which case I am fine with the current solution.
validateRequestBody(body, spec, schemas); | ||
throw new Error('function `validateRequestBody` should throw error.'); |
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 use a message similar to what shouldjs provides:
throw new Error(
'expected Function { name: 'validateRequestBody' } to throw exception'
);
packages/rest/src/rest-http-error.ts
Outdated
dataPath: string; | ||
keyword: string; | ||
message: string; | ||
params: AnyObject; |
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 very happy about using AnyObject
from @loopback/repository
. My understanding is that AnyObject
should be used for values describing plain-data-objects carrying model instance data, e.g. a request body containing Todo data for create
method.
Can we use a different type here please? If object
does not work, then let's use Object
or any
.
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.
👌 changed to object
da375b3
to
8ca6f49
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.
Few more nitpicks to consider, no further review is necessary from my side.
error.details = _.map(ajv.errors, e => { | ||
return _.pick(e, pickedProperties); | ||
return { |
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: I think this can be simplified as follows.
error.details = _.map(ajv.errors, e => {
path: e.dataPath,
// ...
info: e.params,
});
validateRequestBody(body, spec, schemas); | ||
throw new Error('function `validateRequestBody` should throw error.'); | ||
} catch (err) { | ||
expect(err.message).to.equal(errorMatcher); |
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 correct usage would be a single-arg invoke of throw
:
expect(validate).to.throw({
message: /* message text (strict comparison? not ideal) */
details: details
});
Please note that {details: details}
can be shortened to {details}
, and also don't forget to await
the expect statement.
await expect(validate).to.throw({details});
// or
await expect(validate).to.throw({
message: /*...*/,
details,
});
I don't really mind implementation details of verifyValidationRejectsInputWithError
as long as it works correctly in all cases. As I wrote earlier, the current implementation is fine with me.
packages/rest/src/rest-http-error.ts
Outdated
* `ErrorDetails` defines the type of each entry, which is an object. | ||
* The type of `error.details` is `ErrorDetails[]` | ||
*/ | ||
export type ErrorDetails = { |
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 these details are specific to validation errors only (and not to invalidData
error for an example), should use a more specific type name, e.g. ValidationErrorDetails
? Also use export interface
instead of export type
!
packages/rest/src/rest-http-error.ts
Outdated
} | ||
/** | ||
* An invalid request body error contains a `details` property as the machine-readable error. | ||
* Each entry in `error.details` contains the following attributes: |
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 move documentation of individual properties into tsdoc entries for each property. Thoughts?
export interface ErrorDetails {
/**
* A path to the invalid field, e.g. `.name`
*/
path: string;
// ...
};
packages/rest/src/rest-http-error.ts
Outdated
path: string; | ||
code: string; | ||
message: string; | ||
info: 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.
Is the info
(params
) object already filled by AJV? Is it possible that AJV can return undefined
for certain validation rules? In which case info
would need to be marked as an optional property.
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.
ah...params
is a required property, while message
is not:
from ajv source code:
interface ErrorObject {
keyword: string;
dataPath: string;
schemaPath: string;
params: ErrorParameters;
// Added to validation errors of propertyNames keyword schema
propertyName?: string;
// Excluded if messages set to false.
message?: string;
// These are added with the `verbose` option.
schema?: any;
parentSchema?: object;
data?: any;
}
@@ -56,8 +68,17 @@ describe('validateRequestBody', () => { | |||
}); | |||
|
|||
it('rejects data containing values of a wrong type', () => { | |||
const details = [ |
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 consider adding an explicit type information for details
so that the compiler can verify whether the property names below are matching the interface. Same comment applies to other test cases below too.
E.g.
const details: ErrorDetails[] = [
// ...
];
8ca6f49
to
6066291
Compare
validateRequestBody(body, spec, schemas); | ||
throw new Error( |
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 wrap this error in a variable and assert this in the catch block: expect(err).to.not.eql(noErrorThrown)
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 that error doesn't have the same error.message
and error.details
as the catch
block expect to have, so if it happens the 1st assertion expect(err.message).to.equal(errorMatcher);
will faill :)
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 think it's cleaner to directly assert for the error instance created at the moment the validation (unexpectedly) passes, you're right in the sense that the error thrown by AJV is extremely unlikely to have errorMatcher
as its message.
Feel free to ignore this comment
packages/rest/src/rest-http-error.ts
Outdated
/** | ||
* A humnan readable description of the error. | ||
*/ | ||
message?: string; |
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.
// Excluded if messages set to false.
message?: string;
IIUC, we don't set messages: false
in AJV options, therefore AJV will always fill the message for us and this property is always set.
I am proposing to keep ErrorDetails#message
as always provided to make it easier for our clients to handle validation errors. 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.
sounds good reverted it to a required field.
packages/rest/src/rest-http-error.ts
Outdated
* `ErrorDetails` defines the type of each entry, which is an object. | ||
* The type of `error.details` is `ErrorDetails[]`. | ||
*/ | ||
export interface ErrorDetails { |
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.
Re-posting an earlier comment:
Since these details are specific to validation errors only (and not to invalidData
error for an example), should we use a more specific type name, e.g. ValidationErrorDetails
or perhaps ValidationProblem
?
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 sorry I missed that comment, sounds good 👍
packages/rest/src/rest-http-error.ts
Outdated
*/ | ||
info: object; | ||
/** | ||
* A humnan readable description of the error. |
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.
human
36d9d70
to
3518a3e
Compare
3518a3e
to
d872031
Compare
Description
connect to #1489
Original comment from @bajtos
Discussion
LB3 and AJV error details are in different flavours:
codes
,messages
, and details are stored per field. e.g.:For the purpose of localizing error in details, either of those 2 flavour seems good to me.
I tried to convert the ajv style to LB3 style and find it's quite complicated, because
dataPath
cannot always be used as the id for a field.message
but thedataPath
is empty...@bajtos thought? You may have more insights regarding different signatures.